diff --git a/backend/app.js b/backend/app.js index 0cdd414..823ea46 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1192,7 +1192,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { && config.getConfigItem('ytdl_use_twitch_api') && config.getConfigItem('ytdl_twitch_auto_download_chat')) { let vodId = url.split('twitch.tv/videos/')[1]; vodId = vodId.split('?')[0]; - downloadTwitchChatByVODID(vodId, file_name, type, options.user); + twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, options.user); } // renames file if necessary due to bug @@ -1768,42 +1768,6 @@ function removeFileExtension(filename) { return filename_parts.join('.'); } -async function getTwitchChatByFileID(id, type, user_uid, uuid) { - let file_path = null; - - if (user_uid) { - file_path = path.join('users', user_uid, type, id + '.twitch_chat.json'); - } else { - file_path = path.join(type, id + '.twitch_chat.json'); - } - - var chat_file = null; - if (fs.existsSync(file_path)) { - chat_file = fs.readJSONSync(file_path); - } - - return chat_file; -} - -async function downloadTwitchChatByVODID(vodId, id, type, user_uid) { - const twitch_api_key = config_api.getConfigItem('ytdl_twitch_api_key'); - const chat = await twitch_api.getCommentsForVOD(twitch_api_key, vodId); - - // save file if needec params are included - if (id && type) { - let file_path = null; - if (user_uid) { - file_path = path.join('users', user_uid, type, id + '.twitch_chat.json'); - } else { - file_path = path.join(type, id + '.twitch_chat.json'); - } - - if (chat) fs.writeJSONSync(file_path, chat); - } - - return chat; -} - app.use(function(req, res, next) { res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization"); res.header("Access-Control-Allow-Origin", getOrigin()); @@ -2038,7 +2002,7 @@ app.post('/api/getFile', optionalJwt, function (req, res) { } // check if chat exists for twitch videos - if (file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json'); + if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json'); if (file) { res.send({ @@ -2109,11 +2073,12 @@ app.post('/api/getFullTwitchChat', optionalJwt, async (req, res) => { var id = req.body.id; var type = req.body.type; var uuid = req.body.uuid; + var sub = req.body.sub; var user_uid = null; if (req.isAuthenticated()) user_uid = req.user.uid; - const chat_file = await getTwitchChatByFileID(id, type, user_uid, uuid); + const chat_file = await twitch_api.getTwitchChatByFileID(id, type, user_uid, uuid, sub); res.send({ chat: chat_file @@ -2125,28 +2090,19 @@ app.post('/api/downloadTwitchChatByVODID', optionalJwt, async (req, res) => { var type = req.body.type; var vodId = req.body.vodId; var uuid = req.body.uuid; + var sub = req.body.sub; var user_uid = null; if (req.isAuthenticated()) user_uid = req.user.uid; // check if file already exists. if so, send that instead - const file_exists_check = await getTwitchChatByFileID(id, type, user_uid, uuid); + const file_exists_check = await twitch_api.getTwitchChatByFileID(id, type, user_uid, uuid, sub); if (file_exists_check) { res.send({chat: file_exists_check}); return; } - const full_chat = await downloadTwitchChatByVODID(vodId); - - let file_path = null; - - if (user_uid) { - file_path = path.join('users', req.user.uid, type, id + '.twitch_chat.json'); - } else { - file_path = path.join(type, id + '.twitch_chat.json'); - } - - if (full_chat) fs.writeJSONSync(file_path, full_chat); + const full_chat = await twitch_api.downloadTwitchChatByVODID(vodId, id, type, user_uid, sub); res.send({ chat: full_chat @@ -2435,9 +2391,15 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => { var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date); parsed_files.push(file_obj); } + } else { + // loop through files for extra processing + for (let i = 0; i < parsed_files.length; i++) { + const file = parsed_files[i]; + // check if chat exists for twitch videos + if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json'); + } } - res.send({ subscription: subscription, files: parsed_files diff --git a/backend/subscriptions.js b/backend/subscriptions.js index 4bd071b..9ab7542 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -6,7 +6,8 @@ var path = require('path'); var youtubedl = require('youtube-dl'); const config_api = require('./config'); -var utils = require('./utils') +const twitch_api = require('./twitch'); +var utils = require('./utils'); const debugMode = process.env.YTDL_MODE === 'debug'; @@ -418,6 +419,15 @@ function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_ sub_db.get('videos').push(output_json).write(); } else { db_api.registerFileDB(path.basename(output_json['_filename']), sub.type, multiUserMode, sub); + const url = output_json['webpage_url']; + if (sub.type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1 + && config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) { + const file_name = path.basename(output_json['_filename']); + const id = file_name.substring(0, file_name.length-4); + let vodId = url.split('twitch.tv/videos/')[1]; + vodId = vodId.split('?')[0]; + twitch_api.downloadTwitchChatByVODID(vodId, id, sub.type, multiUserMode.user, sub); + } } } diff --git a/backend/twitch.js b/backend/twitch.js index fb05fbb..2a231e9 100644 --- a/backend/twitch.js +++ b/backend/twitch.js @@ -1,5 +1,8 @@ var moment = require('moment'); var Axios = require('axios'); +var fs = require('fs-extra') +var path = require('path'); +const config_api = require('./config'); async function getCommentsForVOD(clientID, vodId) { let url = `https://api.twitch.tv/v5/videos/${vodId}/comments?content_offset_seconds=0`, @@ -68,6 +71,58 @@ async function getCommentsForVOD(clientID, vodId) { return comments; } +async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) { + let file_path = null; + + if (user_uid) { + if (sub) { + file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json'); + } else { + file_path = path.join('users', user_uid, type, id + '.twitch_chat.json'); + } + } else { + if (sub) { + file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json'); + } else { + file_path = path.join(type, id + '.twitch_chat.json'); + } + } + + var chat_file = null; + if (fs.existsSync(file_path)) { + chat_file = fs.readJSONSync(file_path); + } + + return chat_file; +} + +async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) { + const twitch_api_key = config_api.getConfigItem('ytdl_twitch_api_key'); + const chat = await getCommentsForVOD(twitch_api_key, vodId); + + // save file if needed params are included + let file_path = null; + if (user_uid) { + if (sub) { + file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json'); + } else { + file_path = path.join('users', user_uid, type, id + '.twitch_chat.json'); + } + } else { + if (sub) { + file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json'); + } else { + file_path = path.join(type, id + '.twitch_chat.json'); + } + } + + if (chat) fs.writeJSONSync(file_path, chat); + + return chat; +} + module.exports = { - getCommentsForVOD: getCommentsForVOD + getCommentsForVOD: getCommentsForVOD, + getTwitchChatByFileID: getTwitchChatByFileID, + downloadTwitchChatByVODID: downloadTwitchChatByVODID } \ No newline at end of file diff --git a/src/app/components/twitch-chat/twitch-chat.component.html b/src/app/components/twitch-chat/twitch-chat.component.html index 634cac6..43fbac0 100644 --- a/src/app/components/twitch-chat/twitch-chat.component.html +++ b/src/app/components/twitch-chat/twitch-chat.component.html @@ -1,7 +1,8 @@
Twitch Chat
-
+
{{chat.timestamp_str}} - {{chat.name}}: {{chat.message}} + {{last ? scrollToBottom() : ''}}
diff --git a/src/app/components/twitch-chat/twitch-chat.component.ts b/src/app/components/twitch-chat/twitch-chat.component.ts index eace952..d2e0aa0 100644 --- a/src/app/components/twitch-chat/twitch-chat.component.ts +++ b/src/app/components/twitch-chat/twitch-chat.component.ts @@ -21,9 +21,11 @@ export class TwitchChatComponent implements OnInit, AfterViewInit { scrollContainer = null; @Input() db_file = null; + @Input() sub = null; @Input() current_timestamp = null; @ViewChild('scrollContainer') scrollRef: ElementRef; + @ViewChildren('chat') chat: QueryList; constructor(private postsService: PostsService) { } @@ -35,22 +37,31 @@ export class TwitchChatComponent implements OnInit, AfterViewInit { } private isUserNearBottom(): boolean { - const threshold = 300; + const threshold = 150; const position = this.scrollContainer.scrollTop + this.scrollContainer.offsetHeight; const height = this.scrollContainer.scrollHeight; return position > height - threshold; } - scrollToBottom = () => { - this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight; + scrollToBottom = (force_scroll) => { + if (force_scroll || this.isUserNearBottom()) { + this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight; + } } addNewChatMessages() { + const next_chat_index = this.getIndexOfNextChat(); if (!this.scrollContainer) { this.scrollContainer = this.scrollRef.nativeElement; } if (this.current_chat_index === null) { - this.current_chat_index = this.getIndexOfNextChat(); + this.current_chat_index = next_chat_index; + } + + if (Math.abs(next_chat_index - this.current_chat_index) > 25) { + this.visible_chat = []; + this.current_chat_index = next_chat_index - 25; + setTimeout(() => this.scrollToBottom(true), 100); } const latest_chat_timestamp = this.visible_chat.length ? this.visible_chat[this.visible_chat.length - 1]['timestamp'] : 0; @@ -59,9 +70,6 @@ export class TwitchChatComponent implements OnInit, AfterViewInit { if (this.full_chat[i]['timestamp'] >= latest_chat_timestamp && this.full_chat[i]['timestamp'] <= this.current_timestamp) { this.visible_chat.push(this.full_chat[i]); this.current_chat_index = i; - if (this.isUserNearBottom()) { - this.scrollToBottom(); - } } else if (this.full_chat[i]['timestamp'] > this.current_timestamp) { break; } @@ -74,7 +82,7 @@ export class TwitchChatComponent implements OnInit, AfterViewInit { } getFullChat() { - this.postsService.getFullTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', null).subscribe(res => { + this.postsService.getFullTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', null, this.sub).subscribe(res => { this.chat_response_received = true; if (res['chat']) { this.initializeChatCheck(res['chat']); @@ -82,11 +90,6 @@ export class TwitchChatComponent implements OnInit, AfterViewInit { }); } - renewChat() { - this.visible_chat = []; - this.current_chat_index = this.getIndexOfNextChat(); - } - downloadTwitchChat() { this.downloading_chat = true; let vodId = this.db_file.url.split('videos/').length > 1 && this.db_file.url.split('videos/')[1]; @@ -94,7 +97,7 @@ export class TwitchChatComponent implements OnInit, AfterViewInit { if (!vodId) { this.postsService.openSnackBar('VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"'); } - this.postsService.downloadTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', vodId, null).subscribe(res => { + this.postsService.downloadTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', vodId, null, this.sub).subscribe(res => { if (res['chat']) { this.initializeChatCheck(res['chat']); } else { diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index 6774ace..53fad9b 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -15,7 +15,7 @@
- + diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index fe8ff5a..412a583 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -39,6 +39,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { type: string; id = null; // used for playlists (not subscription) uid = null; // used for non-subscription files (audio, video, playlist) + subscription = null; subscriptionName = null; subPlaylist = null; uuid = null; // used for sharing in multi-user mode, uuid is the user that downloaded the video @@ -180,6 +181,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { getSubscription() { this.postsService.getSubscription(null, this.subscriptionName).subscribe(res => { const subscription = res['subscription']; + this.subscription = subscription; if (this.fileNames) { subscription.videos.forEach(video => { if (video['id'] === this.fileNames[0]) { @@ -276,12 +278,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { this.api = api; this.api_ready = true; - this.api.subscriptions.seeked.subscribe(data => { - if (this.twitchChat) { - this.twitchChat.renewChat(); - } - }); - // checks if volume has been previously set. if so, use that as default if (localStorage.getItem('player_volume')) { this.api.volume = parseFloat(localStorage.getItem('player_volume')); diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index da479fb..c3934c7 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -234,12 +234,12 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions); } - getFullTwitchChat(id, type, uuid = null) { - return this.http.post(this.path + 'getFullTwitchChat', {id: id, type: type, uuid: uuid}, this.httpOptions); + getFullTwitchChat(id, type, uuid = null, sub = null) { + return this.http.post(this.path + 'getFullTwitchChat', {id: id, type: type, uuid: uuid, sub: sub}, this.httpOptions); } - downloadTwitchChat(id, type, vodId, uuid = null) { - return this.http.post(this.path + 'downloadTwitchChatByVODID', {id: id, type: type, vodId: vodId, uuid: uuid}, this.httpOptions); + downloadTwitchChat(id, type, vodId, uuid = null, sub = null) { + return this.http.post(this.path + 'downloadTwitchChatByVODID', {id: id, type: type, vodId: vodId, uuid: uuid, sub: sub}, this.httpOptions); } downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,