Compare commits

..

35 Commits

Author SHA1 Message Date
Tzahi12345
cfbee6d6f1 Sub name duplicated bug fix 2023-04-25 23:59:13 -04:00
Tzahi12345
c75d58efd5 Fixed issue where duplicate sub names were possible (#801) 2023-04-25 23:36:15 -04:00
Tzahi12345
efbf395368 Fixed settings tab url labels 2023-04-25 22:14:52 -04:00
Tzahi12345
dab9fc83ba Added support for discord webhooks
Improved download error notifications
2023-04-25 22:14:35 -04:00
Tzahi12345
e086bbc301 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into pre-4.3.1-bug-fixes 2023-04-25 21:57:24 -04:00
Tzahi12345
0b3a21b383 Added missing config file settings 2023-04-24 21:17:33 -04:00
Tzahi12345
f973426bd2 Hotfix for error that prevents downloads from occurring 2023-04-24 21:11:10 -04:00
Tzahi12345
a4c78e3a3d Minor verbose message update 2023-04-24 21:10:51 -04:00
Tzahi12345
50d3bc183b Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into pre-4.3.1-bug-fixes 2023-04-24 20:14:55 -04:00
Tzahi12345
5a379a6a2b Updated package-lock.json (#877) 2023-04-24 20:03:11 -04:00
Tzahi12345
71692f6b13 Updated dependencies 2023-04-24 19:55:53 -04:00
Tzahi12345
1746b08d4c Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into pre-4.3.1-bug-fixes 2023-04-24 19:32:19 -04:00
Tzahi12345
3bc0ec8bb5 Added category tests
Fixed syntax errors in tests.js
2023-04-24 19:30:25 -04:00
Tzahi12345
2df4dc1bfc Updated file not found error message in /stream 2023-04-24 19:29:35 -04:00
Tzahi12345
0e190fca2a Ghost file card count are now per-sub 2023-04-24 19:28:52 -04:00
Tzahi12345
5aea0b7a3d Archive text file is now temporarily added to a sub dir when checking a sub for speed purposes 2023-04-24 19:28:11 -04:00
Tzahi12345
d76aaf83f6 Merge pull request #707 from Tzahi12345/categories-fix
Categories matching bug fix
2023-04-24 10:35:08 -04:00
Tzahi12345
a996b9f0d2 ng-deeps now only apply to current component
https://stackoverflow.com/questions/46786986/how-and-where-to-use-ng-deep
2023-04-23 22:31:11 -04:00
Tzahi12345
d3b88412c6 Fixed thumbnails for auto-generated playlists 2023-04-23 22:27:09 -04:00
Tzahi12345
6cee892e18 Added label for category field in video-info-dialog 2023-04-23 22:20:50 -04:00
Tzahi12345
e2438a236b Adjusted category UI styling to Angular 15 updates 2023-04-23 22:14:20 -04:00
Tzahi12345
955c401f0b Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into categories-fix 2023-04-22 17:47:16 -04:00
Tzahi12345
f0a34df7c6 Fixed syntax error in utils 2023-04-21 16:48:24 -04:00
Tzahi12345
e2c68713ba Fixed bug where added args would get injected improperly if they had a value (#745) 2023-04-21 16:34:47 -04:00
Tzahi12345
24cabc1f02 Fixed issue where videos would be downloaded in the collectInfo stage (#774) 2023-04-21 16:12:41 -04:00
Tzahi12345
1edcfca6c3 Fixed bug where notifications wouldn't be set as read if using local db 2023-04-21 15:57:59 -04:00
Tzahi12345
e7fa25cf38 Fixed tasks UI bug 2023-04-21 15:34:14 -04:00
Tzahi12345
527b1f1cb9 Merge pull request #872 from Tzahi12345/twitch-downloader-fix
Twitch chat download fixes
2023-04-21 00:16:06 -04:00
Tzahi12345
24d8072eb5 Fixed minor syntax error in Dockerfile 2023-04-20 21:47:20 -04:00
Tzahi12345
c81bf980ca Separated image for TwitchDownloader download in Dockerfile 2023-04-20 21:41:36 -04:00
Tzahi12345
a91381720f Added link in error message to get TiwtchDownloaderCLI 2023-04-20 21:16:06 -04:00
Tzahi12345
edd4a0928c Fixed twitch downloading by using TiwtchDownloader's CLI
Removed unecessary settings

Created dedicated folder for docker utils
2023-04-20 21:11:48 -04:00
Isaac Abadi
2e52ec22e0 Default sort for videos is now download date 2022-09-15 21:38:19 -04:00
Isaac Abadi
efdd0dd228 Categories fix for yt-dlp 2022-09-15 21:38:00 -04:00
Isaac Abadi
415c97cb09 Fixed issue where categories were not being properly applied to matching files (#701) 2022-07-07 01:07:22 -04:00
39 changed files with 1614 additions and 2866 deletions

1
.gitignore vendored
View File

@@ -67,7 +67,6 @@ backend/appdata/users.json
backend/users/*
backend/appdata/cookies.txt
backend/public
backend/dist
src/assets/i18n/*.json
# User Files

View File

@@ -2,7 +2,7 @@
FROM ubuntu:22.04 AS ffmpeg
ENV DEBIAN_FRONTEND=noninteractive
# Use script due local build compability
COPY ffmpeg-fetch.sh .
COPY docker-utils/ffmpeg-fetch.sh .
RUN chmod +x ffmpeg-fetch.sh
RUN sh ./ffmpeg-fetch.sh
@@ -47,6 +47,15 @@ RUN npm config set strict-ssl false && \
npm install --prod && \
ls -al
FROM base as python
WORKDIR /app
COPY docker-utils/GetTwitchDownloader.py .
RUN apt update && \
apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip && \
apt clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install PyGithub requests
RUN python GetTwitchDownloader.py
# Final image
FROM base
@@ -55,7 +64,7 @@ RUN npm install -g pm2 && \
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
apt clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install tdh-tcd pycryptodomex
RUN pip install pycryptodomex
WORKDIR /app
# User 1000 already exist from base image
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]

View File

@@ -1,7 +1,3 @@
// TODO: ignore this if not in electron
const rootPath = require('electron-root-path').rootPath;
process.chdir(rootPath);
const { uuid } = require('uuidv4');
const fs = require('fs-extra');
const { promisify } = require('util');
@@ -60,7 +56,48 @@ let debugMode = process.env.YTDL_MODE === 'debug';
const admin_token = '4241b401-7236-493e-92b5-b72696b9d853';
// required initialization
// logging setup
config_api.initialize();
db_api.initialize(db, users_db);
auth_api.initialize(db_api);
// Set some defaults
db.defaults(
{
playlists: [],
files: [],
configWriteFlag: false,
downloads: {},
subscriptions: [],
files_to_db_migration_complete: false,
tasks_manager_role_migration_complete: false,
archives_migration_complete: false
}).write();
users_db.defaults(
{
users: [],
roles: {
"admin": {
"permissions": [
'filemanager',
'settings',
'subscriptions',
'sharing',
'advanced_download',
'downloads_manager'
]
}, "user": {
"permissions": [
'filemanager',
'subscriptions',
'sharing'
]
}
}
}
).write();
// config values
let url = null;
@@ -112,61 +149,20 @@ if (fs.existsSync('version.json')) {
version_info = {'type': 'N/A', 'tag': 'N/A', 'commit': 'N/A', 'date': 'N/A'};
}
exports.initialize = () => {
config_api.initialize();
db_api.initialize(db, users_db);
auth_api.initialize(db_api);
// don't overwrite config if it already happened.. NOT
// let alreadyWritten = db.get('configWriteFlag').value();
// Set some defaults
db.defaults(
{
playlists: [],
files: [],
configWriteFlag: false,
downloads: {},
subscriptions: [],
files_to_db_migration_complete: false,
tasks_manager_role_migration_complete: false,
archives_migration_complete: false
}).write();
// checks if config exists, if not, a config is auto generated
config_api.configExistsCheck();
users_db.defaults(
{
users: [],
roles: {
"admin": {
"permissions": [
'filemanager',
'settings',
'subscriptions',
'sharing',
'advanced_download',
'downloads_manager'
]
}, "user": {
"permissions": [
'filemanager',
'subscriptions',
'sharing'
]
}
}
}
).write();
setAndLoadConfig();
// checks if config exists, if not, a config is auto generated
config_api.configExistsCheck();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
setAndLoadConfig();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// use passport
app.use(auth_api.passport.initialize());
}
exports.initialize();
// use passport
app.use(auth_api.passport.initialize());
app.use(auth_api.passport.session());
// actual functions
@@ -256,7 +252,7 @@ async function simplifyDBFileStructure() {
return true;
}
exports.startServer = async () => {
async function startServer() {
if (process.env.USING_HEROKU && process.env.PORT) {
// default to heroku port if using heroku
backendPort = process.env.PORT || backendPort;
@@ -549,9 +545,7 @@ async function loadConfig() {
}
// start the server here
if (typeof require !== 'undefined' && require.main === module) {
exports.startServer();
}
startServer();
return true;
}
@@ -1645,9 +1639,7 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
else file_path = null;
}
if (!fs.existsSync(file_path)) {
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj.id}`);
res.sendStatus(404);
return;
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
}
const stat = fs.statSync(file_path);
const fileSize = stat.size;

View File

@@ -23,7 +23,12 @@
"download_only_mode": false,
"allow_autoplay": true,
"enable_downloads_manager": true,
"allow_playlist_categorization": true
"allow_playlist_categorization": true,
"force_autoplay": false,
"enable_notifications": true,
"enable_all_notifications": true,
"allowed_notification_types": [],
"enable_rss_feed": false
},
"API": {
"use_API_key": false,
@@ -35,7 +40,17 @@
"twitch_client_secret": "",
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false,
"generate_NFO_files": false
"generate_NFO_files": false,
"use_ntfy_API": false,
"ntfy_topic_URL": "",
"use_gotify_API": false,
"gotify_server_URL": "",
"gotify_app_token": "",
"use_telegram_API": false,
"telegram_bot_token": "",
"telegram_chat_id": "",
"webhook_URL": "",
"discord_webhook_URL": ""
},
"Themes": {
"default_theme": "default",

View File

@@ -208,9 +208,6 @@ 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,
@@ -222,7 +219,8 @@ const DEFAULT_CONFIG = {
"use_telegram_API": false,
"telegram_bot_token": "",
"telegram_chat_id": "",
"webhook_URL": ""
"webhook_URL": "",
"discord_webhook_URL": ""
},
"Themes": {
"default_theme": "default",

View File

@@ -110,18 +110,6 @@ 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'
@@ -170,6 +158,10 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_webhook_url',
'path': 'YoutubeDLMaterial.API.webhook_URL'
},
'ytdl_discord_webhook_url': {
'key': 'ytdl_discord_webhook_url',
'path': 'YoutubeDLMaterial.API.discord_webhook_URL'
},
// Themes
@@ -354,4 +346,6 @@ const YTDL_ARGS_WITH_VALUES = [
// we're using a Set here for performance
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
exports.ICON_URL = 'https://i.imgur.com/IKOlr0N.png';
exports.CURRENT_VERSION = 'v4.3.1';

View File

@@ -720,7 +720,14 @@ exports.updateRecord = async (table, filter_obj, update_obj, nested_mode = false
exports.updateRecords = async (table, filter_obj, update_obj) => {
// local db override
if (using_local_db) {
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write();
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').each((record) => {
const props_to_update = Object.keys(update_obj);
for (let i = 0; i < props_to_update.length; i++) {
const prop_to_update = props_to_update[i];
const prop_value = update_obj[prop_to_update];
record[prop_to_update] = prop_value;
}
}).write();
return true;
}

View File

@@ -128,7 +128,7 @@ exports.clearDownload = async (download_uid) => {
async function handleDownloadError(download, error_message, error_type = null) {
if (!download || !download['uid']) return;
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_type);
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_message, error_type);
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
}
@@ -245,11 +245,10 @@ async function collectInfo(download_uid) {
options.customOutput = category['custom_output'];
options.noRelativePath = true;
args = await exports.generateArgs(url, type, options, download['user_uid']);
args = utils.filterArgs(args, ['--no-simulate']);
info = await exports.getVideoInfoByURL(url, args, download_uid);
}
download['category'] = category;
const stripped_category = category ? {name: category['name'], uid: category['uid']} : null;
// setup info required to calculate download progress
@@ -272,6 +271,7 @@ async function collectInfo(download_uid) {
files_to_check_for_progress: files_to_check_for_progress,
expected_file_size: expected_file_size,
title: playlist_title ? playlist_title : info['title'],
category: stripped_category,
prefetched_info: null
});
}
@@ -314,7 +314,7 @@ async function downloadQueuedFile(download_uid) {
clearInterval(download_checker);
if (err) {
logger.error(err.stderr);
await handleDownloadError(download, err.stderr);
await handleDownloadError(download, err.stderr, 'unknown_error');
resolve(false);
return;
} else if (output) {
@@ -350,7 +350,7 @@ async function downloadQueuedFile(download_uid) {
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
if (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')) {
&& config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
let vodId = url.split('twitch.tv/videos/')[1];
vodId = vodId.split('?')[0];
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
@@ -552,7 +552,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
return new Promise(resolve => {
// remove bad args
const new_args = [...args];
const temp_args = utils.filterArgs(args, ['--no-simulate']);
const new_args = [...temp_args];
const archiveArgIndex = new_args.indexOf('--download-archive');
if (archiveArgIndex !== -1) {
@@ -595,7 +596,7 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
logger.error(error_message);
if (download_uid) {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
await handleDownloadError(download, error_message);
await handleDownloadError(download, error_message, 'info_retrieve_failed');
}
resolve(null);
}

View File

@@ -1,82 +0,0 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
const elogger = require('electron-log');
const server = require('./app');
let win;
let splashWindow;
async function createSplashWindow() {
splashWindow = new BrowserWindow({
width: 800,
height: 600,
frame: false,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: true
}
})
await splashWindow.loadFile('public/assets/splash.html')
splashWindow.on('closed', () => {
splashWindow = null
})
}
function createMainWindow() {
win = new BrowserWindow(
{
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
icon: path.join(__dirname, 'favicon.ico'),
show: false
});
// The following is optional and will open the DevTools:
// win.webContents.openDevTools()
win.on('closed', () => {
win = null;
});
}
function loadPage() {
splashWindow.close()
// load the dist folder from Angular
win.loadURL('http://localhost:17442')
win.show()
}
async function createWindow() {
await createSplashWindow();
elogger.info('Spawning server.')
// serverProcess = spawn('node', [path.join(__dirname, 'app.js')]);
await server.startServer();
elogger.info('Done spawning!')
createMainWindow();
loadPage();
}
app.on('ready', createWindow);
// on macOS, closing the window doesn't quit the app
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// initialize the app's main window
app.on('activate', () => {
if (win === null) {
createWindow();
}
});
process.on('uncaughtException', (error) => {
elogger.error(error.message);
});

View File

@@ -2,12 +2,16 @@ const db_api = require('./db');
const config_api = require('./config');
const logger = require('./logger');
const utils = require('./utils');
const consts = require('./consts');
const { uuid } = require('uuidv4');
const fetch = require('node-fetch');
const { gotify } = require("gotify");
const TelegramBot = require('node-telegram-bot-api');
const REST = require('@discordjs/rest').REST;
const API = require('@discordjs/core').API;
const EmbedBuilder = require('@discordjs/builders').EmbedBuilder;
const NOTIFICATION_TYPE_TO_TITLE = {
task_finished: 'Task finished',
@@ -18,7 +22,7 @@ const NOTIFICATION_TYPE_TO_TITLE = {
const NOTIFICATION_TYPE_TO_BODY = {
task_finished: (notification) => notification['data']['task_title'],
download_complete: (notification) => {return `${notification['data']['file_title']}\nOriginal URL: ${notification['data']['original_url']}`},
download_error: (notification) => {return `Error: ${notification['data']['download_error_type']}\nURL: ${notification['data']['download_url']}`}
download_error: (notification) => {return `Error: ${notification['data']['download_error_message']}\nError code: ${notification['data']['download_error_type']}\n\nOriginal URL: ${notification['data']['download_url']}`}
}
const NOTIFICATION_TYPE_TO_URL = {
@@ -57,6 +61,9 @@ exports.sendNotification = async (notification) => {
if (config_api.getConfigItem('ytdl_webhook_url')) {
sendGenericNotification(data);
}
if (config_api.getConfigItem('ytdl_discord_webhook_url')) {
sendDiscordNotification(data);
}
await db_api.insertRecordIntoTable('notifications', notification);
return notification;
@@ -79,9 +86,9 @@ exports.sendDownloadNotification = async (file, user_uid) => {
return await exports.sendNotification(notification);
}
exports.sendDownloadErrorNotification = async (download, user_uid, error_type = null) => {
exports.sendDownloadErrorNotification = async (download, user_uid, error_message, error_type = null) => {
if (!notificationEnabled('download_error')) return;
const data = {download_uid: download.uid, download_url: download.url, download_error_type: error_type};
const data = {download_uid: download.uid, download_url: download.url, download_error_message: error_message, download_error_type: error_type};
const notification = exports.createNotification('download_error', ['view_download_error', 'retry_download'], data, user_uid);
return await exports.sendNotification(notification);
}
@@ -144,6 +151,29 @@ async function sendTelegramNotification({body, title, type, url, thumbnail}) {
bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
}
async function sendDiscordNotification({body, title, type, url, thumbnail}) {
const discord_webhook_url = config_api.getConfigItem('ytdl_discord_webhook_url');
const url_split = discord_webhook_url.split('webhooks/');
const [webhook_id, webhook_token] = url_split[1].split('/');
const rest = new REST({ version: '10' });
const api = new API(rest);
const embed = new EmbedBuilder()
.setTitle(title)
.setColor(0x00FFFF)
.setURL(url)
.setDescription(`ID: ${type}`);
if (thumbnail) embed.setThumbnail(thumbnail);
if (type === 'download_error') embed.setColor(0xFC2003);
const result = await api.webhooks.execute(webhook_id, webhook_token, {
content: body,
username: 'YoutubeDL-Material',
avatar_url: consts.ICON_URL,
embeds: [embed],
});
return result;
}
function sendGenericNotification(data) {
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
logger.verbose(`Sending generic notification to ${webhook_url}`);

2628
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,44 +2,11 @@
"name": "backend",
"version": "1.0.0",
"description": "backend for YoutubeDL-Material",
"main": "main.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "pm2-runtime --raw pm2.config.js",
"debug": "set YTDL_MODE=debug && node app.js",
"electron": "electron main.js",
"pack": "electron-builder --dir",
"build": "electron-builder"
},
"build": {
"appId": "youtubedl.material",
"productName": "YoutubeDL-Material GUI",
"directories": {
"output": "dist"
},
"mac": {
"category": "public.app-category.utilities",
"icon": "../src/assets/images/logo_512px.png",
"target": "dmg"
},
"win": {
"target": "nsis",
"icon": "../src/favicon.ico"
},
"files": [
"!audio/*",
"!video/*",
"!users/*",
"!subscriptions/*",
"!appdata/*",
"*.js",
"authentication/auth.js",
"main.js",
"public/**/*",
"ffmpeg*",
"ffprobe*"
],
"asar": false
"debug": "set YTDL_MODE=debug && node app.js"
},
"repository": {
"type": "git",
@@ -52,17 +19,17 @@
},
"homepage": "",
"dependencies": {
"app-root-path": "^3.1.0",
"@discordjs/builders": "^1.6.1",
"@discordjs/core": "^0.5.2",
"archiver": "^5.3.1",
"async": "^3.2.3",
"async-mutex": "^0.3.1",
"async-mutex": "^0.4.0",
"axios": "^0.21.2",
"bcryptjs": "^2.4.0",
"compression": "^1.7.4",
"config": "^3.2.3",
"electron-log": "^4.4.8",
"electron-root-path": "^1.1.0",
"express": "^4.17.3",
"express": "^4.18.2",
"express-session": "^1.17.3",
"feed": "^4.2.2",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0",
@@ -90,14 +57,9 @@
"rxjs": "^7.3.0",
"shortid": "^2.2.15",
"unzipper": "^0.10.10",
"uuidv4": "^6.0.6",
"uuidv4": "^6.2.13",
"winston": "^3.7.2",
"xmlbuilder2": "^3.0.2",
"youtube-dl": "^3.0.2"
},
"devDependencies": {
"electron": "^24.1.2",
"electron-builder": "^23.6.0",
"electron-packager": "^17.1.1"
}
}

View File

@@ -1,5 +0,0 @@
const { contextBridge } = require('electron');
const path = require('path');
// Expose the 'path' module to the renderer process
contextBridge.exposeInMainWorld('path', path);

View File

@@ -92,7 +92,10 @@ async function getSubscriptionInfo(sub) {
}
// if it's now valid, update
if (sub.name) {
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub.name});
let sub_name = sub.name;
const sub_name_exists = await db_api.getRecord('subscriptions', {name: sub.name, isPlaylist: sub.isPlaylist, user_uid: sub.user_uid});
if (sub_name_exists) sub_name += ` - ${sub.id}`;
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub_name});
}
}
@@ -229,13 +232,20 @@ async function getVideosForSub(sub, user_uid = null) {
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
// get videos
logger.verbose(`Subscription: getting videos for subscription ${sub.name} with args: ${downloadConfig.join(',')}`);
logger.verbose(`Subscription: getting list of videos to download for ${sub.name} with args: ${downloadConfig.join(',')}`);
return new Promise(async resolve => {
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
// cleanup
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
// remove temporary archive file if it exists
const archive_path = path.join(appendedBasePath, 'archive.txt');
const archive_exists = await fs.pathExists(archive_path);
if (archive_exists) {
await fs.unlink(archive_path);
}
logger.verbose('Subscription: finished check for ' + sub.name);
if (err && !output) {
logger.error(err.stderr ? err.stderr : err.message);
@@ -354,6 +364,16 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
downloadConfig.push(...qualityPath)
// if archive is being used, we want to quickly skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) {
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
const archive_path = path.join(appendedBasePath, 'archive.txt');
await fs.writeFile(archive_path, archive_text);
downloadConfig.push('--download-archive', archive_path);
}
if (sub.custom_args) {
const customArgsArray = sub.custom_args.split(',,');
if (customArgsArray.indexOf('-f') !== -1) {

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
const assert = require('assert');
const low = require('lowdb')
const winston = require('winston');
@@ -38,6 +39,7 @@ var db_api = require('../db');
const utils = require('../utils');
const subscriptions_api = require('../subscriptions');
const archive_api = require('../archive');
const categories_api = require('../categories');
const fs = require('fs-extra');
const { uuid } = require('uuidv4');
const NodeID3 = require('node-id3');
@@ -175,6 +177,15 @@ describe('Database', async function() {
await db_api.removeRecord('test', {test_update: 'test'});
});
it('Update records', async function() {
await db_api.insertRecordIntoTable('test', {test_update: 'test', key: 'test1'});
await db_api.insertRecordIntoTable('test', {test_update: 'test', key: 'test2'});
await db_api.updateRecords('test', {test_update: 'test'}, {added_field: true});
const updated_records = await db_api.getRecords('test', {added_field: true});
assert(updated_records.length === 2);
await db_api.removeRecord('test', {test_update: 'test'});
});
it('Remove property from record', async function() {
await db_api.insertRecordIntoTable('test', {test_keep: 'test', test_remove: 'test'});
await db_api.removePropertyFromRecord('test', {test_keep: 'test'}, {test_remove: true});
@@ -339,8 +350,10 @@ describe('Multi User', async function() {
});
});
describe('Video player - normal', async function() {
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
await db_api.insertRecordIntoTable('files', sample_video_json);
beforeEach(async function() {
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
await db_api.insertRecordIntoTable('files', sample_video_json);
});
const video_to_test = sample_video_json['uid'];
it('Get video', async function() {
const video_obj = await db_api.getVideo(video_to_test);
@@ -497,18 +510,23 @@ describe('Downloader', function() {
const new_args1 = ['--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
const updated_args1 = utils.injectArgs(original_args1, new_args1);
const expected_args1 = ['--no-resize-buffer', '--no-mtime', '--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
assert(JSON.stringify(updated_args1), JSON.stringify(expected_args1));
assert(JSON.stringify(updated_args1) === JSON.stringify(expected_args1));
const original_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3'];
const new_args2 = ['--add-metadata', '--embed-thumbnail', '--convert-thumbnails', 'jpg'];
const updated_args2 = utils.injectArgs(original_args2, new_args2);
const expected_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3', '--add-metadata', '--embed-thumbnail', '--convert_thumbnails', 'jpg'];
console.log(updated_args2);
assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2));
const expected_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3', '--add-metadata', '--embed-thumbnail', '--convert-thumbnails', 'jpg'];
assert(JSON.stringify(updated_args2) === JSON.stringify(expected_args2));
const original_args3 = ['-o', '%(title)s.%(ext)s'];
const new_args3 = ['--min-filesize','1'];
const updated_args3 = utils.injectArgs(original_args3, new_args3);
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 = '1493770675';
const example_vod = '1710641401';
it('Download VOD', async function() {
const sample_path = path.join('test', 'sample.twitch_chat.json');
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
@@ -590,7 +608,7 @@ describe('Tasks', function() {
fs.copyFileSync('test/sample.mp4', 'video/sample.mp4');
await tasks_api.executeTask('missing_db_records');
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
assert(!!imported_file, true);
assert(!!imported_file === true);
// post-test cleanup
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
@@ -728,4 +746,109 @@ describe('Utils', async function() {
assert(nested_obj2['test1'] && nested_obj2['test1']['test_sub']);
assert(nested_obj2['test1'] && nested_obj2['test1']['test2'] && nested_obj2['test1']['test2']['test_sub']);
});
});
describe('Categories', async function() {
beforeEach(async function() {
await db_api.connectToDB();
const new_category = {
name: 'test_category',
uid: uuid(),
rules: [],
custom_output: ''
};
await db_api.insertRecordIntoTable('categories', new_category);
});
afterEach(async function() {
await db_api.removeAllRecords('categories', {name: 'test_category'});
});
it('Categorize - includes', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'includes',
property: 'title',
value: 'Sample'
});
const category = await categories_api.categorize([sample_video_json]);
assert(category && category.name === 'test_category');
});
it('Categorize - not includes', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'not_includes',
property: 'title',
value: 'Sample'
});
const category = await categories_api.categorize([sample_video_json]);
assert(!category);
});
it('Categorize - equals', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'equals',
property: 'uploader',
value: 'Sample Uploader'
});
const category = await categories_api.categorize([sample_video_json]);
console.log(category);
assert(category && category.name === 'test_category');
});
it('Categorize - not equals', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'not_equals',
property: 'uploader',
value: 'Sample Uploader'
});
const category = await categories_api.categorize([sample_video_json]);
assert(!category);
});
it('Categorize - AND', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'equals',
property: 'uploader',
value: 'Sample Uploader'
});
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: 'and',
comparator: 'not_includes',
property: 'title',
value: 'Sample'
});
const category = await categories_api.categorize([sample_video_json]);
assert(!category);
});
it('Categorize - OR', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'equals',
property: 'uploader',
value: 'Sample Uploader'
});
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: 'or',
comparator: 'not_includes',
property: 'title',
value: 'Sample'
});
const category = await categories_api.categorize([sample_video_json]);
assert(category);
});
});

View File

@@ -4,19 +4,28 @@ const logger = require('./logger');
const moment = require('moment');
const fs = require('fs-extra')
const path = require('path');
const { promisify } = require('util');
const child_process = require('child_process');
async function getCommentsForVOD(clientID, clientSecret, vodId) {
const { promisify } = require('util');
const child_process = require('child_process');
async function getCommentsForVOD(vodId) {
const exec = promisify(child_process.exec);
// Reject invalid params to prevent command injection attack
if (!clientID.match(/^[0-9a-z]+$/) || !clientSecret.match(/^[0-9a-z]+$/) || !vodId.match(/^[0-9a-z]+$/)) {
logger.error('Client ID, client secret, and VOD ID must be purely alphanumeric. Twitch chat download failed!');
if (!vodId.match(/^[0-9a-z]+$/)) {
logger.error('VOD ID must be purely alphanumeric. Twitch chat download failed!');
return null;
}
const result = await exec(`tcd --video ${vodId} --client-id ${clientID} --client-secret ${clientSecret} --format json -o appdata`, {stdio:[0,1,2]});
const is_windows = process.platform === 'win32';
const cliExt = is_windows ? '.exe' : ''
const cliPath = `TwitchDownloaderCLI${cliExt}`
if (!fs.existsSync(cliPath)) {
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
return null;
}
const result = await exec(`TwitchDownloaderCLI chatdownload -u ${vodId} -o appdata/${vodId}.json`, {stdio:[0,1,2]});
if (result['stderr']) {
logger.error(`Failed to download twitch comments for ${vodId}`);
@@ -73,9 +82,7 @@ async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
async function downloadTwitchChatByVODID(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 twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
const chat = await getCommentsForVOD(twitch_client_id, twitch_client_secret, vodId);
const chat = await getCommentsForVOD(vodId);
// save file if needed params are included
let file_path = null;

View File

@@ -92,7 +92,7 @@ exports.createZipFile = async (zip_file_path, file_paths) => {
await archive.finalize();
// wait a tiny bit for the zip to reload in fs
await wait(100);
await exports.wait(100);
return zip_file_path;
}
@@ -414,10 +414,11 @@ exports.injectArgs = (original_args, new_args) => {
if (CONSTS.YTDL_ARGS_WITH_VALUES.has(new_arg)) {
if (original_args.includes(new_arg)) {
const original_index = original_args.indexOf(new_arg);
original_args.splice(original_index, 2);
updated_args.splice(original_index, 2);
}
updated_args.push(new_arg, new_args[i + 1]);
i++; // we need to skip the arg value on the next loop
} else {
if (!original_args.includes(new_arg)) {
updated_args.push(new_arg);

View File

@@ -0,0 +1,53 @@
import platform
import requests
import shutil
import os
import re
from github import Github
machine = platform.machine()
def isARM():
return True if machine.startswith('arm') else False
def getLatestFileInRepo(repo, search_string):
# Create an unauthenticated instance of the Github object
g = Github(os.environ.get('GH_TOKEN'))
# Replace with the repository owner and name
repo = g.get_repo(repo)
# Get all releases of the repository
releases = repo.get_releases()
# Loop through the releases in reverse order (from latest to oldest)
for release in list(releases):
# Get the release assets (files attached to the release)
assets = release.get_assets()
# Loop through the assets
for asset in assets:
if re.search(search_string, asset.name):
print(f'Downloading: {asset.name}')
response = requests.get(asset.browser_download_url)
with open(asset.name, 'wb') as f:
f.write(response.content)
print(f'Download complete: {asset.name}. Unzipping...')
shutil.unpack_archive(asset.name, './')
print(f'Unzipping complete!')
os.remove(asset.name)
break
else:
continue
break
else:
# If no matching release is found, print a message
print(f'No release found with {search_string}')
def getLatestCLIRelease():
isArm = isARM()
searchString = r'.*CLI.*' + "LinuxArm.zip" if isArm else "Linux-x64.zip"
getLatestFileInRepo("lay295/TwitchDownloader", searchString)
getLatestCLIRelease()

41
main.js Normal file
View File

@@ -0,0 +1,41 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
const url = require('url');
let win;
function createWindow() {
win = new BrowserWindow({ width: 800, height: 600 });
// load the dist folder from Angular
win.loadURL(
url.format({
pathname: path.join(__dirname, `/dist/index.html`),
protocol: 'file:',
slashes: true
})
);
// The following is optional and will open the DevTools:
// win.webContents.openDevTools()
win.on('closed', () => {
win = null;
});
}
app.on('ready', createWindow);
// on macOS, closing the window doesn't quit the app
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// initialize the app's main window
app.on('activate', () => {
if (win === null) {
createWindow();
}
});

1070
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@
"core-js": "^2.4.1",
"crypto-js": "^4.1.1",
"file-saver": "^2.0.2",
"filesize": "^6.1.0",
"filesize": "^10.0.7",
"fingerprintjs2": "^2.1.0",
"fs-extra": "^10.0.0",
"material-icons": "^1.10.8",
@@ -47,7 +47,7 @@
"ngx-avatars": "^1.4.1",
"ngx-file-drop": "^13.0.0",
"rxjs": "^6.6.3",
"rxjs-compat": "^6.0.0-rc.0",
"rxjs-compat": "^6.6.7",
"tslib": "^2.0.0",
"typescript": "~4.8.4",
"xliff-to-json": "^1.0.4",
@@ -60,20 +60,21 @@
"@angular/language-service": "^15.0.1",
"@types/core-js": "^2.5.2",
"@types/file-saver": "^2.0.1",
"@types/jasmine": "~3.6.0",
"@types/jasmine": "^4.3.1",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"ajv": "^7.2.4",
"codelyzer": "^6.0.0",
"electron": "^19.1.9",
"eslint": "^7.32.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.3.16",
"karma": "~6.4.2",
"karma-chrome-launcher": "~3.1.0",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^1.5.0",
"openapi-typescript-codegen": "^0.23.0",
"protractor": "~7.0.0",

View File

@@ -25,7 +25,7 @@
-webkit-line-clamp: 2;
}
::ng-deep .ngx-file-drop__content {
:host ::ng-deep .ngx-file-drop__content {
width: 100%;
top: -12px;
position: relative;

View File

@@ -2,7 +2,7 @@
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''" [loading]="false"></app-unified-file-card>
</div>
</div>
</div>

View File

@@ -76,8 +76,9 @@ export class RecentVideosComponent implements OnInit {
constructor(public postsService: PostsService, private router: Router) {
// get cached file count
if (localStorage.getItem('cached_file_count')) {
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
const sub_id_appendix = this.sub_id ? `_${this.sub_id}` : ''
if (localStorage.getItem(`cached_file_count${sub_id_appendix}`)) {
this.cached_file_count = +localStorage.getItem(`cached_file_count${sub_id_appendix}`) <= 10 ? +localStorage.getItem(`cached_file_count${sub_id_appendix}`) : 10;
this.loading_files = Array(this.cached_file_count).fill(0);
}

View File

@@ -63,7 +63,7 @@
<ng-container i18n="Update binary to">Update binary to:</ng-container>&nbsp;{{element.data}}
</ng-container>
<ng-container *ngIf="element.key == 'delete_old_files'">
<ng-container i18n="Delete old files">Delete old files:</ng-container>&nbsp;{{element.data.uids.length}}
<ng-container i18n="Delete old files">Delete old files:</ng-container>&nbsp;{{element.data.files_to_remove.length}}
</ng-container>
</button>
</ng-container>

View File

@@ -31,6 +31,6 @@ mat-header-cell, mat-cell {
border-radius: 16px 16px 16px 16px !important;
}
::ng-deep mat-row {
:host ::ng-deep mat-row {
height: fit-content !important;
}

View File

@@ -171,6 +171,6 @@
}
::ng-deep.mat-mdc-menu-panel {
:host ::ng-deep.mat-mdc-menu-panel {
max-width: none !important;
}

View File

@@ -8,10 +8,10 @@
top: -12px;
}
::ng-deep.mat-menu-panel {
:host ::ng-deep.mat-menu-panel {
max-width: none !important;
}
::ng-deep.mdc-list-item__primary-text {
:host ::ng-deep.mdc-list-item__primary-text {
width: 100%;
}

View File

@@ -31,9 +31,11 @@
<mat-form-field class="value-input">
<input matInput [(ngModel)]="rule['value']">
</mat-form-field>
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
<span class="rule-buttons">
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
</span>
</mat-list-item>
</mat-list>

View File

@@ -1,5 +1,5 @@
.operator-select {
width: 55px;
width: 90px;
}
.property-select {
@@ -13,4 +13,17 @@
.value-input {
margin-left: 10px;
}
:host ::ng-deep.mdc-list-item {
height: 75px !important;
}
:host ::ng-deep.mdc-list-item__content {
pointer-events: unset;
}
.rule-buttons {
position: relative;
top: 8px;
}

View File

@@ -37,7 +37,8 @@
<input [(ngModel)]="new_file.thumbnailURL" matInput [disabled]="!editing || new_file.thumbnailPath">
</mat-form-field>
<mat-form-field *ngIf="initialized && postsService.categories" class="info-field">
<mat-select placeholder="Category" i18n-placeholder="Category" [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-label i18n="Category">Category</mat-label>
<mat-select [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-option [value]="{}">
N/A
</mat-option>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, Inject } from '@angular/core';
import filesize from 'filesize';
import { filesize } from 'filesize';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { PostsService } from 'app/posts.services';
import { Category, DatabaseFile } from 'api-types';

View File

@@ -38,7 +38,7 @@
</ng-container>
<ng-container *ngIf="db_file || playlist[currentIndex]"></ng-container>
<button (click)="openFileInfoDialog()" *ngIf="db_file || db_playlist" mat-icon-button><mat-icon>info</mat-icon></button>
<button *ngIf="db_file && db_file.url.includes('twitch.tv') && postsService['config']['API']['use_twitch_API']" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
<button *ngIf="db_file && db_file.url.includes('twitch.tv')" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
</div>
</div>
</div>
@@ -51,7 +51,7 @@
<app-concurrent-stream *ngIf="db_file && api && postsService.config" (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists'] && postsService['config']['API']['use_twitch_API']">
<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')">
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
</ng-container>

View File

@@ -269,25 +269,9 @@
<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 *ngIf="new_config['API']['use_twitch_API']" class="col-12 mt-1">
<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>
@@ -401,6 +385,13 @@
<mat-hint>Place endpoint URL here to integrate with services like Zapier and Automatisch.</mat-hint>
</mat-form-field>
</div>
<div class="col-12 mb-2 mt-3">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="Discord webhook URL">Discord Webhook URL</mat-label>
<input placeholder="https://discord.com/api/webhooks/<webhook_id>/<webhook_token>" [(ngModel)]="new_config['API']['discord_webhook_URL']" matInput>
<mat-hint><a target="_blank" href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"><ng-container i18n="Gotify API setting hint">See docs here.</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-3">
<mat-checkbox color="accent" [disabled]="!new_config['Extra']['enable_notifications']" [(ngModel)]="new_config['API']['use_ntfy_API']"><ng-container i18n="Use ntfy API setting">Use ntfy API</ng-container></mat-checkbox>
</div>

View File

@@ -7,7 +7,7 @@
margin-top: 15px;
}
::ng-deep .mat-mdc-tab-body {
:host ::ng-deep .mat-mdc-tab-body {
margin-left: 15px;
}

View File

@@ -41,7 +41,7 @@ export class SettingsComponent implements OnInit {
latestGithubRelease = null;
CURRENT_VERSION = CURRENT_VERSION
tabs = ['main', 'downloader', 'extra', 'database', 'advanced', 'users', 'logs'];
tabs = ['main', 'downloader', 'extra', 'database', 'notifications', 'advanced', 'users', 'logs'];
tabIndex = 0;
INDEX_TO_TAB = Object.assign({}, this.tabs);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Splash Screen</title>
<style>
body {
background-color: #222;
margin: 0;
padding: 0;
}
.center {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
</head>
<body>
<div class="center">
<img src="images/logo_128px.png" alt="Logo">
</div>
</body>
</html>