Compare commits

...

10 Commits

Author SHA1 Message Date
Tzahi12345
148297d558 Fixed applyFilterLocalDB 2023-06-03 17:16:11 -04:00
Tzahi12345
7167c362d2 Fixed issue where filtering DB item with missing fields with local db would crash the server 2023-06-03 16:09:23 -04:00
Tzahi12345
8ac0ac2976 Updated and complete Twitch emoticon logic in the frontend and backend 2023-06-03 16:08:27 -04:00
Tzahi12345
3a20e03490 Added ability to download twitch emotes in the backend 2023-05-29 23:41:30 -04:00
Tzahi12345
7124792721 Updated recent-videos for angular 16 2023-05-29 22:08:24 -04:00
Tzahi12345
b32396164d Updated ngx-avatars 2023-05-29 21:52:10 -04:00
Tzahi12345
3d633f9e47 Updated angular material to 16 2023-05-29 21:51:18 -04:00
Tzahi12345
cfa0a62587 Updated angular core and cli to 16 2023-05-29 21:48:26 -04:00
Tzahi12345
1d53f6b1b6 Reintroduced twitch API settings
Added twitch-emoticons support, began initial work of adding twitch emoticons to twitch chat
2023-05-29 21:42:13 -04:00
Tzahi12345
398b2c0e1c Updated export method for twitch.js 2023-05-29 21:21:32 -04:00
16 changed files with 27233 additions and 3423 deletions

View File

@@ -706,7 +706,7 @@ app.use(function(req, res, next) {
next();
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
next();
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss')) {
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/emote/') || req.path.includes('/api/rss')) {
next();
} else {
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
@@ -993,11 +993,11 @@ app.post('/api/updateConcurrentStream', optionalJwt, async (req, res) => {
});
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;
const id = req.body.id;
const type = req.body.type;
const uuid = req.body.uuid;
const sub = req.body.sub;
let user_uid = null;
if (req.isAuthenticated()) user_uid = req.user.uid;
@@ -1009,12 +1009,12 @@ app.post('/api/getFullTwitchChat', optionalJwt, async (req, res) => {
});
app.post('/api/downloadTwitchChatByVODID', optionalJwt, async (req, res) => {
var id = req.body.id;
var type = req.body.type;
var vodId = req.body.vodId;
var uuid = req.body.uuid;
var sub = req.body.sub;
var user_uid = null;
const id = req.body.id;
const type = req.body.type;
const vodId = req.body.vodId;
const uuid = req.body.uuid;
const sub = req.body.sub;
let user_uid = null;
if (req.isAuthenticated()) user_uid = req.user.uid;
@@ -1025,17 +1025,47 @@ app.post('/api/downloadTwitchChatByVODID', optionalJwt, async (req, res) => {
return;
}
logger.info(`Downloading Twitch chat for ${id}`);
const full_chat = await twitch_api.downloadTwitchChatByVODID(vodId, id, type, user_uid, sub);
logger.info(`Finished downloading Twitch chat for ${id}`);
res.send({
chat: full_chat
});
});
app.post('/api/getTwitchEmotes', async (req, res) => {
const uid = req.body.uid;
const file = await files_api.getVideo(uid);
const channel_name = file['uploader'];
const emotes_path = path.join('appdata', 'emotes', uid, 'emotes.json')
if (!fs.existsSync(emotes_path)) {
logger.info(`Downloading Twitch emotes for ${channel_name}`);
await twitch_api.downloadTwitchEmotes(channel_name, file.uid);
logger.info(`Finished downloading Twitch emotes for ${channel_name}`);
}
const emotes = await twitch_api.getTwitchEmotes(file.uid);
res.send({
emotes: emotes
});
});
app.get('/api/emote/:uid/:id', async (req, res) => {
const file_uid = decodeURIComponent(req.params.uid);
const emote_id = decodeURIComponent(req.params.id);
const emote_path = path.join('appdata', 'emotes', file_uid, emote_id);
if (fs.existsSync(emote_path)) path.isAbsolute(emote_path) ? res.sendFile(emote_path) : res.sendFile(path.join(__dirname, emote_path));
else res.sendStatus(404);
});
// video sharing
app.post('/api/enableSharing', optionalJwt, async (req, res) => {
var uid = req.body.uid;
var is_playlist = req.body.is_playlist;
const uid = req.body.uid;
const is_playlist = req.body.is_playlist;
let success = false;
// multi-user mode
if (req.isAuthenticated()) {
@@ -1643,6 +1673,8 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
}
if (!fs.existsSync(file_path)) {
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
res.sendStatus(404);
return;
}
const stat = fs.statSync(file_path);
const fileSize = stat.size;

View File

@@ -208,6 +208,9 @@ const DEFAULT_CONFIG = {
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_client_ID": "",
"twitch_client_secret": "",
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false,
"generate_NFO_files": false,

View File

@@ -110,6 +110,18 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_youtube_api_key',
'path': 'YoutubeDLMaterial.API.youtube_API_key'
},
'ytdl_use_twitch_api': {
'key': 'ytdl_use_twitch_api',
'path': 'YoutubeDLMaterial.API.use_twitch_API'
},
'ytdl_twitch_client_id': {
'key': 'ytdl_twitch_client_id',
'path': 'YoutubeDLMaterial.API.twitch_client_ID'
},
'ytdl_twitch_client_secret': {
'key': 'ytdl_twitch_client_secret',
'path': 'YoutubeDLMaterial.API.twitch_client_secret'
},
'ytdl_twitch_auto_download_chat': {
'key': 'ytdl_twitch_auto_download_chat',
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'

View File

@@ -815,6 +815,9 @@ exports.applyFilterLocalDB = (db_path, filter_obj, operation) => {
filtered &= record[filter_prop] === undefined || record[filter_prop] === null;
} else {
if (typeof filter_prop_value === 'object') {
if (!record[filter_prop]) {
continue;
}
if ('$regex' in filter_prop_value) {
filtered &= (record[filter_prop].search(new RegExp(filter_prop_value['$regex'], filter_prop_value['$options'])) !== -1);
} else if ('$ne' in filter_prop_value) {
@@ -830,10 +833,14 @@ exports.applyFilterLocalDB = (db_path, filter_obj, operation) => {
}
} else {
// handle case of nested property check
if (filter_prop.includes('.'))
if (filter_prop.includes('.')) {
filtered &= utils.searchObjectByString(record, filter_prop) === filter_prop_value;
else
} else {
if (!record[filter_prop]) {
continue;
}
filtered &= record[filter_prop] === filter_prop_value;
}
}
}
}

6074
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@
"dependencies": {
"@discordjs/builders": "^1.6.1",
"@discordjs/core": "^0.5.2",
"@tzahi12345/twitch-emoticons": "^1.0.9",
"archiver": "^5.3.1",
"async": "^3.2.3",
"async-mutex": "^0.4.0",

View File

@@ -550,27 +550,69 @@ describe('Downloader', function() {
const expected_args3 = ['-o', '%(title)s.%(ext)s', '--min-filesize', '1'];
assert(JSON.stringify(updated_args3) === JSON.stringify(expected_args3));
});
describe('Twitch', async function () {
const twitch_api = require('../twitch');
const example_vod = '1710641401';
it('Download VOD chat', async function() {
this.timeout(300000);
if (!fs.existsSync('TwitchDownloaderCLI')) {
try {
await exec('sh ../docker-utils/fetch-twitchdownloader.sh');
fs.copyFileSync('../docker-utils/TwitchDownloaderCLI', 'TwitchDownloaderCLI');
} catch (e) {
logger.info('TwitchDownloaderCLI fetch failed, file may exist regardless.');
}
}
const sample_path = path.join('test', 'sample.twitch_chat.json');
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
await twitch_api.downloadTwitchChatByVODID(example_vod, 'sample', null, null, null, './test');
assert(fs.existsSync(sample_path));
});
// cleanup
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
});
describe('Twitch', async function () {
const twitch_api = require('../twitch');
const example_vod = '1710641401';
const example_channel = 'keffals';
it('Get OAuth Token', async function() {
this.timeout(300000);
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
if (!twitch_client_id || !twitch_client_secret) {
logger.info(`Skipping test 'Get OAuth Token' as Twitch client ID or Twitch client secret is missing.`);
assert(true);
return;
}
const token = await twitch_api.getTwitchOAuthToken(twitch_client_id, twitch_client_secret);
assert(token);
});
it('Get channel ID', async function() {
this.timeout(300000);
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
if (!twitch_client_id || !twitch_client_secret) {
logger.info(`Skipping test 'Get channel ID' as Twitch client ID or Twitch client secret is missing.`);
assert(true);
return;
}
const channel_id = await twitch_api.getChannelID(example_channel);
assert(channel_id === '494493142');
});
it('Download VOD chat', async function() {
this.timeout(300000);
if (!fs.existsSync('TwitchDownloaderCLI')) {
try {
await exec('sh ../docker-utils/fetch-twitchdownloader.sh');
fs.copyFileSync('../docker-utils/TwitchDownloaderCLI', 'TwitchDownloaderCLI');
} catch (e) {
logger.info('TwitchDownloaderCLI fetch failed, file may exist regardless.');
}
}
const sample_path = path.join('test', 'sample.twitch_chat.json');
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
await twitch_api.downloadTwitchChatByVODID(example_vod, 'sample', null, null, null, './test');
assert(fs.existsSync(sample_path));
// cleanup
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
});
it('Download Twitch emotes', async function() {
this.timeout(300000);
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
if (!twitch_client_id || !twitch_client_secret) {
logger.info(`Skipping test 'Download Twitch emotes' as Twitch client ID or Twitch client secret is missing.`);
assert(true);
return;
}
const emotesJSON = await twitch_api.downloadTwitchEmotes(example_channel, 'test_uid');
assert(emotesJSON && emotesJSON.length > 0);
});
});

View File

@@ -1,14 +1,32 @@
const config_api = require('./config');
const logger = require('./logger');
const utils = require('./utils');
const moment = require('moment');
const fs = require('fs-extra')
const axios = require('axios');
const { EmoteFetcher } = require('@tzahi12345/twitch-emoticons');
const path = require('path');
const { promisify } = require('util');
const child_process = require('child_process');
const commandExistsSync = require('command-exists').sync;
async function getCommentsForVOD(vodId) {
let auth_timeout = null;
let cached_oauth = null;
exports.ensureTwitchAuth = async () => {
const TIMEOUT_MARGIN_MS = 60*1000;
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
if (cached_oauth && auth_timeout && (Date.now() - TIMEOUT_MARGIN_MS) < auth_timeout) return cached_oauth;
const {token, expires_in} = await exports.getTwitchOAuthToken(twitch_client_id, twitch_client_secret);
cached_oauth = token;
auth_timeout = Date.now() + expires_in;
return token;
}
exports.getCommentsForVOD = async (vodId) => {
const exec = promisify(child_process.exec);
// Reject invalid params to prevent command injection attack
@@ -52,7 +70,7 @@ async function getCommentsForVOD(vodId) {
return new_json;
}
async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
exports.getTwitchChatByFileID = async (id, type, user_uid, uuid, sub) => {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
let file_path = null;
@@ -80,10 +98,10 @@ async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
return chat_file;
}
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
exports.downloadTwitchChatByVODID = async (vodId, id, type, user_uid, sub, customFileFolderPath = null) => {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
const chat = await getCommentsForVOD(vodId);
const chat = await exports.getCommentsForVOD(vodId);
// save file if needed params are included
let file_path = null;
@@ -108,6 +126,114 @@ async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customF
return chat;
}
exports.getTwitchEmotes = async (file_uid) => {
const emotes_path = path.join('appdata', 'emotes', file_uid, 'emotes.json')
if (!fs.existsSync(emotes_path)) return null;
const emote_objs = fs.readJSONSync(emotes_path);
// inject custom url
for (const emote_obj of emote_objs) {
emote_obj.custom_url = `${utils.getBaseURL()}/api/emote/${file_uid}/${emote_obj.id}.${emote_obj.ext}`
}
return emote_objs;
}
exports.downloadTwitchEmotes = async (channel_name, file_uid) => {
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
const channel_id = await exports.getChannelID(channel_name);
const fetcher = new EmoteFetcher(twitch_client_id, twitch_client_secret);
try {
await Promise.allSettled([
fetcher.fetchTwitchEmotes(),
fetcher.fetchTwitchEmotes(channel_id),
fetcher.fetchBTTVEmotes(),
fetcher.fetchBTTVEmotes(channel_id),
fetcher.fetchSevenTVEmotes(),
fetcher.fetchSevenTVEmotes(channel_id),
fetcher.fetchFFZEmotes(),
fetcher.fetchFFZEmotes(channel_id)
]);
const emotes_dir = path.join('appdata', 'emotes', file_uid);
const emote_json_path = path.join(emotes_dir, `emotes.json`);
fs.ensureDirSync(emotes_dir);
const emote_objs = [];
let failed_emote_count = 0;
for (const [, emote] of fetcher.emotes) {
const emote_obj = emote.toObject();
const ext = emote.imageType;
const emote_image_path = path.join(emotes_dir, `${emote.id}.${ext}`);
try {
const link = emote.toLink();
if (!fs.existsSync(emote_image_path)) await utils.fetchFile(link, emote_image_path);
emote_obj['ext'] = ext;
emote_objs.push(emote_obj);
} catch (err) {
failed_emote_count++;
}
}
if (failed_emote_count) logger.warn(`${failed_emote_count} emotes failed to download for channel ${channel_name}`);
await fs.writeJSON(emote_json_path, emote_objs);
return emote_objs;
} catch (err) {
logger.error(err);
return null;
}
}
exports.getTwitchOAuthToken = async (client_id, client_secret) => {
logger.verbose('Generating new Twitch auth token');
const url = `https://id.twitch.tv/oauth2/token`;
try {
const response = await axios.post(url, {client_id: client_id, client_secret: client_secret, grant_type: 'client_credentials'});
const token = response['data']['access_token'];
const expires_in = response['data']['expires_in'];
if (token) return {token, expires_in};
logger.error(`Failed to get token.`);
return null;
} catch (err) {
logger.error(`Failed to get token.`);
logger.error(err);
return null;
}
}
exports.getChannelID = async (channel_name) => {
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const token = await exports.ensureTwitchAuth();
const url = `https://api.twitch.tv/helix/users?login=${channel_name}`;
const headers = {
'Client-ID': twitch_client_id,
'Authorization': 'Bearer ' + token,
// Accept: 'application/vnd.twitchtv.v5+json; charset=UTF-8'
};
try {
const response = await axios.get(url, {headers: headers});
const data = response.data.data;
if (data && data.length > 0) {
const channelID = data[0].id;
return channelID;
}
logger.error(`Failed to get channel ID for user ${channel_name}`);
if (data.error) logger.error(data.error);
return null; // User not found
} catch (err) {
logger.error(`Failed to get channel ID for user ${channel_name}`);
logger.error(err);
}
}
const convertTimestamp = (timestamp) => moment.duration(timestamp, 'seconds')
.toISOString()
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
@@ -115,9 +241,3 @@ const convertTimestamp = (timestamp) => moment.duration(timestamp, 'seconds')
const seg = v => v ? v.padStart(2, '0') : '00';
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
});
module.exports = {
getCommentsForVOD: getCommentsForVOD,
getTwitchChatByFileID: getTwitchChatByFileID,
downloadTwitchChatByVODID: downloadTwitchChatByVODID
}

View File

@@ -364,26 +364,29 @@ exports.checkExistsWithTimeout = async (filePath, timeout) => {
}
// helper function to download file using fetch
exports.fetchFile = async (url, path, file_label) => {
exports.fetchFile = async (url, output_path, file_label = null) => {
var len = null;
const res = await fetch(url);
let bar = null;
if (file_label) {
len = parseInt(res.headers.get("Content-Length"), 10);
len = parseInt(res.headers.get("Content-Length"), 10);
bar = new ProgressBar(` Downloading ${file_label} [:bar] :percent :etas`, {
complete: '=',
incomplete: ' ',
width: 20,
total: len
});
}
var bar = new ProgressBar(` Downloading ${file_label} [:bar] :percent :etas`, {
complete: '=',
incomplete: ' ',
width: 20,
total: len
});
const fileStream = fs.createWriteStream(path);
const fileStream = fs.createWriteStream(output_path);
await new Promise((resolve, reject) => {
res.body.pipe(fileStream);
res.body.on("error", (err) => {
reject(err);
});
res.body.on('data', function (chunk) {
bar.tick(chunk.length);
if (file_label) bar.tick(chunk.length);
});
fileStream.on("finish", function() {
resolve();

24143
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,20 +21,21 @@
},
"private": true,
"dependencies": {
"@angular-devkit/core": "^15.0.1",
"@angular/animations": "^15.0.1",
"@angular/cdk": "^15.0.0",
"@angular/common": "^15.0.1",
"@angular/compiler": "^15.0.1",
"@angular/core": "^15.0.1",
"@angular/forms": "^15.0.1",
"@angular/localize": "^15.0.1",
"@angular/material": "^15.0.0",
"@angular/platform-browser": "^15.0.1",
"@angular/platform-browser-dynamic": "^15.0.1",
"@angular/router": "^15.0.1",
"@angular-devkit/core": "^16.0.3",
"@angular/animations": "^16.0.3",
"@angular/cdk": "^16.0.2",
"@angular/common": "^16.0.3",
"@angular/compiler": "^16.0.3",
"@angular/core": "^16.0.3",
"@angular/forms": "^16.0.3",
"@angular/localize": "^16.0.3",
"@angular/material": "^16.0.2",
"@angular/platform-browser": "^16.0.3",
"@angular/platform-browser-dynamic": "^16.0.3",
"@angular/router": "^16.0.3",
"@fontsource/material-icons": "^4.5.4",
"@ngneat/content-loader": "^7.0.0",
"@tzahi12345/twitch-emoticons": "^1.0.9",
"@videogular/ngx-videogular": "^6.0.0",
"core-js": "^2.4.1",
"crypto-js": "^4.1.1",
@@ -43,20 +44,20 @@
"fs-extra": "^10.0.0",
"material-icons": "^1.10.8",
"nan": "^2.14.1",
"ngx-avatars": "^1.4.1",
"ngx-avatars": "^1.6.1",
"ngx-file-drop": "^15.0.0",
"rxjs": "^6.6.3",
"rxjs-compat": "^6.6.7",
"tslib": "^2.0.0",
"typescript": "~4.8.4",
"typescript": "~5.0.4",
"xliff-to-json": "^1.0.4",
"zone.js": "~0.11.4"
"zone.js": "~0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.0.1",
"@angular/cli": "^15.0.1",
"@angular/compiler-cli": "^15.0.1",
"@angular/language-service": "^15.0.1",
"@angular-devkit/build-angular": "^16.0.3",
"@angular/cli": "^16.0.3",
"@angular/compiler-cli": "^16.0.3",
"@angular/language-service": "^16.0.3",
"@types/core-js": "^2.5.2",
"@types/file-saver": "^2.0.1",
"@types/jasmine": "^4.3.1",

View File

@@ -380,8 +380,8 @@ export class RecentVideosComponent implements OnInit {
fileSelectionChanged(event: MatSelectionListChange): void {
// TODO: make sure below line is possible (_selected is private)
const adding = event.option['_selected'];
const value = event.option.value;
const adding = event.options[0]['_selected'];
const value = event.options[0].value;
if (adding) {
this.selected_data.push(value.uid);
this.selected_data_objs.push(value);

View File

@@ -1,7 +1,7 @@
<div class="chat-container" #scrollContainer *ngIf="visible_chat">
<div style="width: 250px; text-align: center;"><strong>Twitch Chat</strong></div>
<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}}
{{chat.timestamp_str}} - <strong [style.color]="chat.user_color ? chat.user_color : null">{{chat.name}}</strong>: <span [innerHTML]="chat.message"></span>
{{last ? scrollToBottom() : ''}}
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { Component, ElementRef, Input, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { DatabaseFile } from 'api-types';
import { PostsService } from 'app/posts.services';
import { EmoteFetcher, EmoteObject, EmoteParser } from '@tzahi12345/twitch-emoticons';
@Component({
selector: 'app-twitch-chat',
@@ -13,6 +14,7 @@ export class TwitchChatComponent implements OnInit, OnDestroy {
visible_chat = null;
chat_response_received = false;
downloading_chat = false;
got_emotes = false;
current_chat_index = null;
@@ -21,6 +23,9 @@ export class TwitchChatComponent implements OnInit, OnDestroy {
scrollContainer = null;
fetcher: EmoteFetcher;
parser: EmoteParser;
@Input() db_file: DatabaseFile = null;
@Input() sub = null;
@Input() current_timestamp = null;
@@ -32,6 +37,7 @@ export class TwitchChatComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.getFullChat();
this.getEmotes();
}
ngOnDestroy(): void {
@@ -69,10 +75,12 @@ export class TwitchChatComponent implements OnInit, OnDestroy {
const latest_chat_timestamp = this.visible_chat.length ? this.visible_chat[this.visible_chat.length - 1]['timestamp'] : 0;
for (let i = this.current_chat_index + 1; i < this.full_chat.length; i++) {
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]);
const new_chat = this.full_chat[i];
if (new_chat['timestamp'] >= latest_chat_timestamp && new_chat['timestamp'] <= this.current_timestamp) {
new_chat['message'] = this.got_emotes ? this.parseChat(new_chat['message']) : new_chat['message'];
this.visible_chat.push(new_chat);
this.current_chat_index = i;
} else if (this.full_chat[i]['timestamp'] > this.current_timestamp) {
} else if (new_chat['timestamp'] > this.current_timestamp) {
break;
}
}
@@ -118,6 +126,29 @@ export class TwitchChatComponent implements OnInit, OnDestroy {
this.chat_check_interval_obj = setInterval(() => this.addNewChatMessages(), this.CHAT_CHECK_INTERVAL_MS);
}
getEmotes() {
this.postsService.getTwitchEmotes(this.db_file['uid']).subscribe(res => {
const emotes = res['emotes'];
this.processEmotes(emotes);
});
}
processEmotes(emotes: EmoteObject[]) {
this.fetcher = new EmoteFetcher();
this.parser = new EmoteParser(this.fetcher, {
// Custom HTML format
template: `<img class="emote" alt="{name}" src="{link}">`,
// Match without :colons:
match: /(\w+)+?/g
});
this.fetcher.fromObject(emotes);
this.got_emotes = true;
}
parseChat(chat_message: string) {
return this.parser.parse(chat_message);
}
}
function binarySearch(arr, key, n) {

View File

@@ -4,7 +4,7 @@ import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { THEMES_CONFIG } from '../themes';
import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { Router, ActivatedRouteSnapshot } from '@angular/router';
import { DOCUMENT } from '@angular/common';
import { BehaviorSubject, Observable } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
@@ -118,9 +118,10 @@ import {
import { isoLangs } from './dialogs/user-profile-dialog/locales_list';
import { Title } from '@angular/platform-browser';
import { MatDrawerMode } from '@angular/material/sidenav';
import type { EmoteObject } from '@tzahi12345/twitch-emoticons';
@Injectable()
export class PostsService implements CanActivate {
export class PostsService {
path = '';
// local settings
@@ -407,6 +408,10 @@ export class PostsService implements CanActivate {
return this.http.post<DownloadTwitchChatByVODIDResponse>(this.path + 'downloadTwitchChatByVODID', body, this.httpOptions);
}
getTwitchEmotes(uid: string) {
return this.http.post<{emotes: EmoteObject[]}>(this.path + 'getTwitchEmotes', {uid: uid}, this.httpOptions);
}
downloadPlaylistFromServer(playlist_id, uuid = null) {
const body: DownloadFileRequest = {uuid: uuid, playlist_id: playlist_id};
return this.http.post(this.path + 'downloadFileFromServer', body, {responseType: 'blob', params: this.httpOptions.params});

View File

@@ -252,9 +252,25 @@
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_twitch_API']"><ng-container i18n="Use Twitch API setting">Use Twitch API</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-1">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="Twitch Client ID">Twitch Client ID</mat-label>
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_ID']" matInput required>
<mat-hint><a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch Client ID setting hint">Generating an ID/secret is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="Twitch Client Secret">Twitch Client Secret</mat-label>
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_secret']" matInput required>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
</div>