mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-26 06:30:58 +03:00
Twitch chat now supports subscriptions
- refactored code to be cleaner and more modularized Updated scrolling on twitch chat to actually scroll to the bottom with new messages Fast forwarding in videos with a twitch chat is now faster and provides a smoother transition
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
<div class="chat-container" #scrollContainer *ngIf="visible_chat">
|
||||
<div style="width: 250px; text-align: center;"><strong>Twitch Chat</strong></div>
|
||||
<div style="max-width: 250px" *ngFor="let chat of visible_chat">
|
||||
<div #chat style="max-width: 250px" *ngFor="let chat of visible_chat; let last = last">
|
||||
{{chat.timestamp_str}} - <strong [style.color]="chat.user_color ? chat.user_color : null">{{chat.name}}</strong>: {{chat.message}}
|
||||
{{last ? scrollToBottom() : ''}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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<any>;
|
||||
|
||||
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 {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists']">
|
||||
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv/videos/')">
|
||||
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime"></app-twitch-chat>
|
||||
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
|
||||
</ng-container>
|
||||
</mat-drawer>
|
||||
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user