Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
a73dd29f24 Bump socket.io-parser from 4.2.2 to 4.2.4
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.2.2 to 4.2.4.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/4.2.2...4.2.4)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-06 21:49:53 +00:00
28 changed files with 392 additions and 874 deletions

View File

@@ -19,22 +19,26 @@ ENV USER=youtube
ENV NO_UPDATE_NOTIFIER=true
ENV PM2_HOME=/app/pm2
ENV ALLOW_CONFIG_MUTATIONS=true
# Use NVM to get specific node version
ENV NODE_VERSION=16.14.2
# Directy fetch specific version
## https://deb.nodesource.com/node_16.x/pool/main/n/nodejs/nodejs_16.14.2-deb-1nodesource1_amd64.deb
RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
apt update && \
apt install -y --no-install-recommends curl ca-certificates tzdata libicu70 libatomic1 && \
apt install -y --no-install-recommends curl ca-certificates tzdata libicu70 && \
apt clean && \
rm -rf /var/lib/apt/lists/*
RUN case ${TARGETPLATFORM} in \
"linux/amd64") NODE_ARCH=amd64 ;; \
"linux/arm") NODE_ARCH=armhf ;; \
"linux/arm/v7") NODE_ARCH=armhf ;; \
"linux/arm64") NODE_ARCH=arm64 ;; \
esac \
&& curl -L https://deb.nodesource.com/node_16.x/pool/main/n/nodejs/nodejs_16.14.2-deb-1nodesource1_$NODE_ARCH.deb -o ./nodejs.deb && \
apt update && \
apt install -y ./nodejs.deb && \
apt clean && \
rm -rf /var/lib/apt/lists/* &&\
rm nodejs.deb;
RUN mkdir /usr/local/nvm
ENV PATH="/usr/local/nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"
ENV NVM_DIR=/usr/local/nvm
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
# Build frontend
ARG BUILDPLATFORM

View File

@@ -293,48 +293,6 @@ paths:
$ref: '#/components/schemas/UnsubscribeResponse'
security:
- Auth query parameter: []
/api/checkSubscription:
post:
tags:
- subscriptions
summary: Run a check for videos for a subscription
description: Runs a subscription check
operationId: post-api-checksubscription
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CheckSubscriptionRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
security:
- Auth query parameter: []
/api/cancelCheckSubscription:
post:
tags:
- subscriptions
summary: Cancels check for videos for a subscription
description: Cancels subscription check
operationId: post-api-checksubscription
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CheckSubscriptionRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
security:
- Auth query parameter: []
/api/deleteSubscriptionFile:
post:
tags:
@@ -2023,11 +1981,11 @@ components:
type: string
UnsubscribeRequest:
required:
- sub_id
- sub
type: object
properties:
sub_id:
type: string
sub:
$ref: '#/components/schemas/SubscriptionRequestData'
deleteMode:
type: boolean
description: Defaults to false
@@ -2040,13 +1998,6 @@ components:
type: boolean
error:
type: string
CheckSubscriptionRequest:
required:
- sub_id
type: object
properties:
sub_id:
type: string
DeleteAllFilesResponse:
type: object
properties:
@@ -2732,8 +2683,6 @@ components:
type: boolean
paused:
type: boolean
cancelled:
type: boolean
finished_step:
type: boolean
url:
@@ -2892,8 +2841,6 @@ components:
nullable: true
isPlaylist:
type: boolean
child_process:
type: object
archive:
type: string
timerange:
@@ -2902,10 +2849,6 @@ components:
type: string
custom_output:
type: string
downloading:
type: boolean
paused:
type: boolean
videos:
type: array
items:

View File

@@ -20,6 +20,11 @@ const ps = require('ps-node');
const Feed = require('feed').Feed;
const session = require('express-session');
// needed if bin/details somehow gets deleted
if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2000.06.06","path":"node_modules\\youtube-dl\\bin\\youtube-dl.exe","exec":"youtube-dl.exe","downloader":"youtube-dl"})
const youtubedl = require('youtube-dl');
const logger = require('./logger');
const config_api = require('./config.js');
const downloader_api = require('./downloader');
@@ -30,7 +35,6 @@ const twitch_api = require('./twitch');
const youtubedl_api = require('./youtube-dl');
const archive_api = require('./archive');
const files_api = require('./files');
const notifications_api = require('./notifications');
var app = express();
@@ -532,7 +536,7 @@ async function loadConfig() {
// set downloading to false
let subscriptions = await subscriptions_api.getAllSubscriptions();
subscriptions.forEach(async sub => subscriptions_api.writeSubscriptionMetadata(sub));
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false, child_process: null});
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false});
// runs initially, then runs every ${subscriptionCheckInterval} seconds
const watchSubscriptionsInterval = function() {
watchSubscriptions();
@@ -616,7 +620,7 @@ async function watchSubscriptions() {
logger.verbose(`Skipping subscription ${sub.name} due to multi-user mode change.`);
return;
}
await subscriptions_api.getVideosForSub(sub.id);
await subscriptions_api.getVideosForSub(sub, sub.user_uid);
subscription_timeouts[sub.id] = false;
}, current_delay);
subscription_timeouts[sub.id] = true;
@@ -653,20 +657,36 @@ function generateEnvVarConfigItem(key) {
// currently only works for single urls
async function getUrlInfos(url) {
const {parsed_output, err} = await youtubedl_api.runYoutubeDL(url, ['--dump-json']);
if (!parsed_output || parsed_output.length !== 1) {
logger.error(`Failed to retrieve available formats for url: ${url}`);
if (err) logger.error(err);
return null;
}
return parsed_output[0];
let startDate = Date.now();
let result = [];
return new Promise(resolve => {
youtubedl.exec(url, ['--dump-json'], {maxBuffer: Infinity}, (err, output) => {
let new_date = Date.now();
let difference = (new_date - startDate)/1000;
logger.debug(`URL info retrieval delay: ${difference} seconds.`);
if (err) {
logger.error(`Error during retrieving formats for ${url}: ${err}`);
resolve(null);
}
let try_putput = null;
try {
try_putput = JSON.parse(output);
result = try_putput;
} catch(e) {
logger.error(`Failed to retrieve available formats for url: ${url}`);
}
resolve(result);
});
});
}
// youtube-dl functions
async function startYoutubeDL() {
// auto update youtube-dl
await youtubedl_api.checkForYoutubeDLUpdate();
youtubedl_api.verifyBinaryExistsLinux();
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
}
app.use(function(req, res, next) {
@@ -686,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') || req.path.includes('/api/telegramRequest')) {
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss')) {
next();
} else {
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
@@ -1192,10 +1212,10 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => {
app.post('/api/unsubscribe', optionalJwt, async (req, res) => {
let deleteMode = req.body.deleteMode
let sub_id = req.body.sub_id;
let sub = req.body.sub;
let user_uid = req.isAuthenticated() ? req.user.uid : null;
let result_obj = subscriptions_api.unsubscribe(sub_id, deleteMode, user_uid);
let result_obj = subscriptions_api.unsubscribe(sub, deleteMode, user_uid);
if (result_obj.success) {
res.send({
success: result_obj.success
@@ -1265,49 +1285,21 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
});
app.post('/api/downloadVideosForSubscription', optionalJwt, async (req, res) => {
const subID = req.body.subID;
let subID = req.body.subID;
let user_uid = req.isAuthenticated() ? req.user.uid : null;
const sub = subscriptions_api.getSubscription(subID);
subscriptions_api.getVideosForSub(sub.id);
let sub = subscriptions_api.getSubscription(subID, user_uid);
subscriptions_api.getVideosForSub(sub, user_uid);
res.send({
success: true
});
});
app.post('/api/updateSubscription', optionalJwt, async (req, res) => {
const updated_sub = req.body.subscription;
const success = subscriptions_api.updateSubscription(updated_sub);
res.send({
success: success
});
});
app.post('/api/checkSubscription', optionalJwt, async (req, res) => {
let sub_id = req.body.sub_id;
let updated_sub = req.body.subscription;
let user_uid = req.isAuthenticated() ? req.user.uid : null;
const success = subscriptions_api.getVideosForSub(sub_id, user_uid);
res.send({
success: success
});
});
app.post('/api/cancelCheckSubscription', optionalJwt, async (req, res) => {
let sub_id = req.body.sub_id;
let user_uid = req.isAuthenticated() ? req.user.uid : null;
const success = subscriptions_api.cancelCheckSubscription(sub_id, user_uid);
res.send({
success: success
});
});
app.post('/api/cancelSubscriptionCheck', optionalJwt, async (req, res) => {
let sub_id = req.body.sub_id;
let user_uid = req.isAuthenticated() ? req.user.uid : null;
const success = subscriptions_api.getVideosForSub(sub_id, user_uid);
let success = subscriptions_api.updateSubscription(updated_sub, user_uid);
res.send({
success: success
});
@@ -1785,10 +1777,6 @@ app.post('/api/cancelDownload', optionalJwt, async (req, res) => {
app.post('/api/getTasks', optionalJwt, async (req, res) => {
const tasks = await db_api.getRecords('tasks');
for (let task of tasks) {
if (!tasks_api.TASKS[task['key']]) {
logger.verbose(`Task ${task['key']} does not exist!`);
continue;
}
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task['key']]['job'].nextInvocation().getTime();
}
res.send({tasks: tasks});
@@ -2097,25 +2085,6 @@ app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => {
res.send({success: success});
});
app.post('/api/telegramRequest', async (req, res) => {
if (!req.body.message && !req.body.message.text) {
logger.error('Invalid Telegram request received!');
res.sendStatus(400);
return;
}
const text = req.body.message.text;
const regex_exp = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi;
const url_regex = new RegExp(regex_exp);
if (text.match(url_regex)) {
downloader_api.createDownload(text, 'video', {}, req.query.user_uid ? req.query.user_uid : null);
res.sendStatus(200);
} else {
logger.error('Invalid Telegram request received! Make sure you only send a valid URL.');
notifications_api.sendTelegramNotification({title: 'Invalid Telegram Request', body: 'Make sure you only send a valid URL.', url: text});
res.sendStatus(400);
}
});
// rss feed
app.get('/api/rss', async function (req, res) {
@@ -2183,8 +2152,6 @@ app.use(function(req, res, next) {
let index_path = path.join(__dirname, 'public', 'index.html');
res.setHeader('Content-Type', 'text/html');
fs.createReadStream(index_path).pipe(res);
});

View File

@@ -49,7 +49,6 @@
"use_telegram_API": false,
"telegram_bot_token": "",
"telegram_chat_id": "",
"telegram_webhook_proxy": "",
"webhook_URL": "",
"discord_webhook_URL": "",
"slack_webhook_URL": ""

View File

@@ -32,8 +32,10 @@ async function categorize(file_jsons) {
return null;
}
for (const file_json of file_jsons) {
for (const category of categories) {
for (let i = 0; i < file_jsons.length; i++) {
const file_json = file_jsons[i];
for (let j = 0; j < categories.length; j++) {
const category = categories[j];
const rules = category['rules'];
// if rules for current category apply, then that is the selected category

View File

@@ -1,26 +1,22 @@
const logger = require('./logger');
const fs = require('fs');
const { BehaviorSubject } = require('rxjs');
exports.CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
exports.descriptors = {}; // to get rid of file locks when needed, TODO: move to youtube-dl.js
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
const debugMode = process.env.YTDL_MODE === 'debug';
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
exports.config_updated = new BehaviorSubject();
exports.initialize = () => {
function initialize() {
ensureConfigFileExists();
ensureConfigItemsExist();
}
function ensureConfigItemsExist() {
const config_keys = Object.keys(exports.CONFIG_ITEMS);
const config_keys = Object.keys(CONFIG_ITEMS);
for (let i = 0; i < config_keys.length; i++) {
const config_key = config_keys[i];
exports.getConfigItem(config_key);
getConfigItem(config_key);
}
}
@@ -61,17 +57,17 @@ function getElementNameInConfig(path) {
/**
* Check if config exists. If not, write default config to config path
*/
exports.configExistsCheck = () => {
function configExistsCheck() {
let exists = fs.existsSync(configPath);
if (!exists) {
exports.setConfigFile(DEFAULT_CONFIG);
setConfigFile(DEFAULT_CONFIG);
}
}
/*
* Gets config file and returns as a json
*/
exports.getConfigFile = () => {
function getConfigFile() {
try {
let raw_data = fs.readFileSync(configPath);
let parsed_data = JSON.parse(raw_data);
@@ -82,40 +78,35 @@ exports.getConfigFile = () => {
}
}
exports.setConfigFile = (config) => {
function setConfigFile(config) {
try {
const old_config = exports.getConfigFile();
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
const changes = exports.findChangedConfigItems(old_config, config);
if (changes.length > 0) {
for (const change of changes) exports.config_updated.next(change);
}
return true;
} catch(e) {
return false;
}
}
exports.getConfigItem = (key) => {
let config_json = exports.getConfigFile();
if (!exports.CONFIG_ITEMS[key]) {
function getConfigItem(key) {
let config_json = getConfigFile();
if (!CONFIG_ITEMS[key]) {
logger.error(`Config item with key '${key}' is not recognized.`);
return null;
}
let path = exports.CONFIG_ITEMS[key]['path'];
let path = CONFIG_ITEMS[key]['path'];
const val = Object.byString(config_json, path);
if (val === undefined && Object.byString(DEFAULT_CONFIG, path) !== undefined) {
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
exports.setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
return Object.byString(DEFAULT_CONFIG, path);
}
return Object.byString(config_json, path);
}
exports.setConfigItem = (key, value) => {
function setConfigItem(key, value) {
let success = false;
let config_json = exports.getConfigFile();
let path = exports.CONFIG_ITEMS[key]['path'];
let config_json = getConfigFile();
let path = CONFIG_ITEMS[key]['path'];
let element_name = getElementNameInConfig(path);
let parent_path = getParentPath(path);
let parent_object = Object.byString(config_json, parent_path);
@@ -127,18 +118,20 @@ exports.setConfigItem = (key, value) => {
parent_parent_object[parent_parent_single_key] = {};
parent_object = Object.byString(config_json, parent_path);
}
if (value === 'false') value = false;
if (value === 'true') value = true;
parent_object[element_name] = value;
success = exports.setConfigFile(config_json);
if (value === 'false' || value === 'true') {
parent_object[element_name] = (value === 'true');
} else {
parent_object[element_name] = value;
}
success = setConfigFile(config_json);
return success;
}
exports.setConfigItems = (items) => {
function setConfigItems(items) {
let success = false;
let config_json = exports.getConfigFile();
let config_json = getConfigFile();
for (let i = 0; i < items.length; i++) {
let key = items[i].key;
let value = items[i].value;
@@ -148,7 +141,7 @@ exports.setConfigItems = (items) => {
value = (value === 'true');
}
let item_path = exports.CONFIG_ITEMS[key]['path'];
let item_path = CONFIG_ITEMS[key]['path'];
let item_parent_path = getParentPath(item_path);
let item_element_name = getElementNameInConfig(item_path);
@@ -156,41 +149,28 @@ exports.setConfigItems = (items) => {
item_parent_object[item_element_name] = value;
}
success = exports.setConfigFile(config_json);
success = setConfigFile(config_json);
return success;
}
exports.globalArgsRequiresSafeDownload = () => {
const globalArgs = exports.getConfigItem('ytdl_custom_args').split(',,');
function globalArgsRequiresSafeDownload() {
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
return failedArgs && failedArgs.length > 0;
}
exports.findChangedConfigItems = (old_config, new_config, path = '', changedConfigItems = [], depth = 0) => {
if (typeof old_config === 'object' && typeof new_config === 'object' && depth < 3) {
for (const key in old_config) {
if (Object.prototype.hasOwnProperty.call(new_config, key)) {
exports.findChangedConfigItems(old_config[key], new_config[key], `${path}${path ? '.' : ''}${key}`, changedConfigItems, depth + 1);
}
}
} else {
if (JSON.stringify(old_config) !== JSON.stringify(new_config)) {
const key = getConfigItemKeyByPath(path);
changedConfigItems.push({
key: key ? key : path.split('.')[path.split('.').length - 1], // return key in CONFIG_ITEMS or the object key
old_value: JSON.parse(JSON.stringify(old_config)),
new_value: JSON.parse(JSON.stringify(new_config))
});
}
}
return changedConfigItems;
}
function getConfigItemKeyByPath(path) {
const found_item = Object.values(exports.CONFIG_ITEMS).find(item => item.path === path);
if (found_item) return found_item['key'];
else return null;
module.exports = {
getConfigItem: getConfigItem,
setConfigItem: setConfigItem,
setConfigItems: setConfigItems,
getConfigFile: getConfigFile,
setConfigFile: setConfigFile,
configExistsCheck: configExistsCheck,
CONFIG_ITEMS: CONFIG_ITEMS,
initialize: initialize,
descriptors: {},
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
}
const DEFAULT_CONFIG = {
@@ -239,7 +219,6 @@ const DEFAULT_CONFIG = {
"use_telegram_API": false,
"telegram_bot_token": "",
"telegram_chat_id": "",
"telegram_webhook_proxy": "",
"webhook_URL": "",
"discord_webhook_URL": "",
"slack_webhook_URL": "",

View File

@@ -154,10 +154,6 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_telegram_chat_id',
'path': 'YoutubeDLMaterial.API.telegram_chat_id'
},
'ytdl_telegram_webhook_proxy': {
'key': 'ytdl_telegram_webhook_proxy',
'path': 'YoutubeDLMaterial.API.telegram_webhook_proxy'
},
'ytdl_webhook_url': {
'key': 'ytdl_webhook_url',
'path': 'YoutubeDLMaterial.API.webhook_URL'
@@ -273,8 +269,7 @@ exports.AVAILABLE_PERMISSIONS = [
'tasks_manager'
];
exports.DETAILS_BIN_PATH = 'appdata/youtube-dl.json'
exports.OUTDATED_YOUTUBEDL_VERSION = "2020.00.00";
exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details'
// args that have a value after it (e.g. -o <output> or -f <format>)
const YTDL_ARGS_WITH_VALUES = [

View File

@@ -4,6 +4,8 @@ const path = require('path');
const NodeID3 = require('node-id3')
const Mutex = require('async-mutex').Mutex;
const youtubedl = require('youtube-dl');
const logger = require('./logger');
const youtubedl_api = require('./youtube-dl');
const config_api = require('./config');
@@ -19,8 +21,6 @@ const archive_api = require('./archive');
const mutex = new Mutex();
let should_check_downloads = true;
const download_to_child_process = {};
if (db_api.database_initialized) {
exports.setupDownloads();
} else {
@@ -84,11 +84,8 @@ exports.pauseDownload = async (download_uid) => {
} else if (download['finished']) {
logger.info(`Download ${download_uid} could not be paused before completing.`);
return false;
} else {
logger.info(`Pausing download ${download_uid}`);
}
killActiveDownload(download);
return await db_api.updateRecord('download_queue', {uid: download_uid}, {paused: true, running: false});
}
@@ -123,23 +120,16 @@ exports.cancelDownload = async (download_uid) => {
} else if (download['finished']) {
logger.info(`Download ${download_uid} could not be cancelled before completing.`);
return false;
} else {
logger.info(`Cancelling download ${download_uid}`);
}
killActiveDownload(download);
await handleDownloadError(download_uid, 'Cancelled', 'cancelled');
return await db_api.updateRecord('download_queue', {uid: download_uid}, {cancelled: true});
return await db_api.updateRecord('download_queue', {uid: download_uid}, {cancelled: true, running: false});
}
exports.clearDownload = async (download_uid) => {
return await db_api.removeRecord('download_queue', {uid: download_uid});
}
async function handleDownloadError(download_uid, error_message, error_type = null) {
if (!download_uid) return;
const download = await db_api.getRecord('download_queue', {uid: download_uid});
if (!download || download['error']) return;
async function handleDownloadError(download, error_message, error_type = null) {
if (!download || !download['uid']) return;
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});
}
@@ -190,14 +180,14 @@ async function checkDownloads() {
if (waiting_download['sub_id']) {
const sub_missing = !(await db_api.getRecord('subscriptions', {id: waiting_download['sub_id']}));
if (sub_missing) {
handleDownloadError(waiting_download['uid'], `Download failed as subscription with id '${waiting_download['sub_id']}' is missing!`, 'sub_id_missing');
handleDownloadError(waiting_download, `Download failed as subscription with id '${waiting_download['sub_id']}' is missing!`, 'sub_id_missing');
continue;
}
}
// move to next step
running_downloads_count++;
if (waiting_download['step_index'] === 0) {
exports.collectInfo(waiting_download['uid']);
collectInfo(waiting_download['uid']);
} else if (waiting_download['step_index'] === 1) {
exports.downloadQueuedFile(waiting_download['uid']);
}
@@ -205,15 +195,7 @@ async function checkDownloads() {
}
}
function killActiveDownload(download) {
const child_process = download_to_child_process[download['uid']];
if (download['step_index'] === 2 && child_process) {
youtubedl_api.killYoutubeDLProcess(child_process);
delete download_to_child_process[download['uid']];
}
}
exports.collectInfo = async (download_uid) => {
async function collectInfo(download_uid) {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
if (download['paused']) {
return;
@@ -236,21 +218,21 @@ exports.collectInfo = async (download_uid) => {
// get video info prior to download
let info = download['prefetched_info'] ? download['prefetched_info'] : await exports.getVideoInfoByURL(url, args, download_uid);
if (!info || info.length === 0) {
if (!info) {
// info failed, error presumably already recorded
return;
}
// in subscriptions we don't care if archive mode is enabled, but we already removed archived videos from subs by this point
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive && !options.ignoreArchive && info.length === 1) {
const info_obj = info[0];
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info_obj['id'], type, download['user_uid'], download['sub_id']);
if (useYoutubeDLArchive && !options.ignoreArchive) {
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info['id'], type, download['user_uid'], download['sub_id']);
if (exists_in_archive) {
const error = `File '${info_obj['title']}' already exists in archive! Disable the archive or override to continue downloading.`;
const error = `File '${info['title']}' already exists in archive! Disable the archive or override to continue downloading.`;
logger.warn(error);
if (download_uid) {
await handleDownloadError(download_uid, error, 'exists_in_archive');
const download = await db_api.getRecord('download_queue', {uid: download_uid});
await handleDownloadError(download, error, 'exists_in_archive');
return;
}
}
@@ -259,7 +241,7 @@ exports.collectInfo = async (download_uid) => {
let category = null;
// check if it fits into a category. If so, then get info again using new args
if (info.length === 1 || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
if (info.length === 0 || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
if (category && category['custom_output']) {
@@ -280,20 +262,20 @@ exports.collectInfo = async (download_uid) => {
// store info in download for future use
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
const title = info.length > 1 ? info[0]['playlist_title'] || info[0]['playlist'] : info[0]['title'];
const playlist_title = info.length > 0 ? info[0]['playlist_title'] || info[0]['playlist'] : null;
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
finished_step: true,
running: false,
options: options,
files_to_check_for_progress: files_to_check_for_progress,
expected_file_size: expected_file_size,
title: title,
title: playlist_title ? playlist_title : info['title'],
category: stripped_category,
prefetched_info: null
});
}
exports.downloadQueuedFile = async(download_uid, customDownloadHandler = null) => {
exports.downloadQueuedFile = async(download_uid, downloadMethod = youtubedl.exec) => {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
if (download['paused']) {
return;
@@ -323,25 +305,21 @@ exports.downloadQueuedFile = async(download_uid, customDownloadHandler = null) =
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
const file_objs = [];
// download file
let {child_process, callback} = await youtubedl_api.runYoutubeDL(url, args, customDownloadHandler);
if (child_process) download_to_child_process[download['uid']] = child_process;
const {parsed_output, err} = await callback;
const {parsed_output, err} = await youtubedl_api.runYoutubeDL(url, args, downloadMethod);
clearInterval(download_checker);
let end_time = Date.now();
let difference = (end_time - start_time)/1000;
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
if (!parsed_output) {
const errored_download = await db_api.getRecord('download_queue', {uid: download_uid});
if (errored_download && errored_download['paused']) return;
logger.error(err.toString());
await handleDownloadError(download_uid, err.toString(), 'unknown_error');
logger.error(err.stderr);
await handleDownloadError(download, err.stderr, 'unknown_error');
resolve(false);
return;
} else if (parsed_output) {
if (parsed_output.length === 0 || parsed_output[0].length === 0) {
// ERROR!
const error_message = `No output received for video download, check if it exists in your archive.`;
await handleDownloadError(download_uid, error_message, 'no_output');
await handleDownloadError(download, error_message, 'no_output');
logger.warn(error_message);
resolve(false);
return;
@@ -407,13 +385,14 @@ exports.downloadQueuedFile = async(download_uid, customDownloadHandler = null) =
if (file_objs.length > 1) {
// create playlist
container = await files_api.createPlaylist(download['title'], file_objs.map(file_obj => file_obj.uid), download['user_uid']);
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
container = await files_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
} else if (file_objs.length === 1) {
container = file_objs[0];
} else {
const error_message = 'Downloaded file failed to result in metadata object.';
logger.error(error_message);
await handleDownloadError(download_uid, error_message, 'no_metadata');
await handleDownloadError(download, error_message, 'no_metadata');
}
const file_uids = file_objs.map(file_obj => file_obj.uid);
@@ -426,7 +405,7 @@ exports.downloadQueuedFile = async(download_uid, customDownloadHandler = null) =
// helper functions
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
if (!simulated && (default_downloader === 'youtube-dl' || default_downloader === 'youtube-dlc')) {
logger.warn('It is recommended you use yt-dlp! To prevent failed downloads, change the downloader in your settings menu to yt-dlp and restart your instance.')
@@ -557,30 +536,34 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
}
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
// remove bad args
const temp_args = utils.filterArgs(args, ['--no-simulate']);
const new_args = [...temp_args];
return new Promise(resolve => {
// remove bad args
const temp_args = utils.filterArgs(args, ['--no-simulate']);
const new_args = [...temp_args];
const archiveArgIndex = new_args.indexOf('--download-archive');
if (archiveArgIndex !== -1) {
new_args.splice(archiveArgIndex, 2);
}
new_args.push('--dump-json');
let {callback} = await youtubedl_api.runYoutubeDL(url, new_args);
const {parsed_output, err} = await callback;
if (!parsed_output || parsed_output.length === 0) {
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
if (err.stderr) error_message += `\n\n${err.stderr}`;
logger.error(error_message);
if (download_uid) {
await handleDownloadError(download_uid, error_message, 'info_retrieve_failed');
const archiveArgIndex = new_args.indexOf('--download-archive');
if (archiveArgIndex !== -1) {
new_args.splice(archiveArgIndex, 2);
}
return null;
}
return parsed_output;
new_args.push('--dump-json');
youtubedl.exec(url, new_args, {maxBuffer: Infinity}, async (err, output) => {
const parsed_output = utils.parseOutputJSON(output, err);
if (parsed_output) {
resolve(parsed_output);
} else {
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
if (err.stderr) error_message += `\n\n${err.stderr}`;
logger.error(error_message);
if (download_uid) {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
await handleDownloadError(download, error_message, 'info_retrieve_failed');
}
resolve(null);
}
});
});
}
function filterArgs(args, isAudio) {
@@ -599,7 +582,6 @@ async function checkDownloadPercent(download_uid) {
*/
const download = await db_api.getRecord('download_queue', {uid: download_uid});
if (!download) return;
const files_to_check_for_progress = download['files_to_check_for_progress'];
const resulting_file_size = download['expected_file_size'];

View File

@@ -8,8 +8,7 @@ const { uuid } = require('uuidv4');
const fetch = require('node-fetch');
const { gotify } = require("gotify");
const TelegramBotAPI = require('node-telegram-bot-api');
let telegram_bot = null;
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;
@@ -57,7 +56,7 @@ exports.sendNotification = async (notification) => {
sendGotifyNotification(data);
}
if (config_api.getConfigItem('ytdl_use_telegram_API') && config_api.getConfigItem('ytdl_telegram_bot_token') && config_api.getConfigItem('ytdl_telegram_chat_id')) {
exports.sendTelegramNotification(data);
sendTelegramNotification(data);
}
if (config_api.getConfigItem('ytdl_webhook_url')) {
sendGenericNotification(data);
@@ -114,8 +113,6 @@ function notificationEnabled(type) {
return config_api.getConfigItem('ytdl_enable_notifications') && (config_api.getConfigItem('ytdl_enable_all_notifications') || config_api.getConfigItem('ytdl_allowed_notification_types').includes(type));
}
// ntfy
function sendNtfyNotification({body, title, type, url, thumbnail}) {
logger.verbose('Sending notification to ntfy');
fetch(config_api.getConfigItem('ytdl_ntfy_topic_url'), {
@@ -130,8 +127,6 @@ function sendNtfyNotification({body, title, type, url, thumbnail}) {
});
}
// Gotify
async function sendGotifyNotification({body, title, type, url, thumbnail}) {
logger.verbose('Sending notification to gotify');
await gotify({
@@ -150,50 +145,15 @@ async function sendGotifyNotification({body, title, type, url, thumbnail}) {
});
}
// Telegram
setupTelegramBot();
config_api.config_updated.subscribe(change => {
const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API');
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
if (!use_telegram_api || !bot_token) return;
if (!change) return;
if (change['key'] === 'ytdl_use_telegram_API' || change['key'] === 'ytdl_telegram_bot_token' || change['key'] === 'ytdl_telegram_webhook_proxy') {
logger.debug('Telegram bot setting up');
setupTelegramBot();
}
});
async function setupTelegramBot() {
const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API');
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
if (!use_telegram_api || !bot_token) return;
telegram_bot = new TelegramBotAPI(bot_token);
const webhook_proxy = config_api.getConfigItem('ytdl_telegram_webhook_proxy');
const webhook_url = webhook_proxy ? webhook_proxy : `${utils.getBaseURL()}/api/telegramRequest`;
telegram_bot.setWebHook(webhook_url);
}
exports.sendTelegramNotification = async ({body, title, type, url, thumbnail}) => {
if (!telegram_bot){
logger.error('Telegram bot not found!');
return;
}
const chat_id = config_api.getConfigItem('ytdl_telegram_chat_id');
if (!chat_id){
logger.error('Telegram chat ID required!');
return;
}
async function sendTelegramNotification({body, title, type, url, thumbnail}) {
logger.verbose('Sending notification to Telegram');
if (thumbnail) await telegram_bot.sendPhoto(chat_id, thumbnail);
telegram_bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
const chat_id = config_api.getConfigItem('ytdl_telegram_chat_id');
const bot = new TelegramBot(bot_token);
if (thumbnail) await bot.sendPhoto(chat_id, thumbnail);
bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
}
// Discord
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/');
@@ -217,8 +177,6 @@ async function sendDiscordNotification({body, title, type, url, thumbnail}) {
return result;
}
// Slack
function sendSlackNotification({body, title, type, url, thumbnail}) {
const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url');
logger.verbose(`Sending slack notification to ${slack_webhook_url}`);
@@ -278,8 +236,6 @@ function sendSlackNotification({body, title, type, url, thumbnail}) {
});
}
// Generic
function sendGenericNotification(data) {
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
logger.verbose(`Sending generic notification to ${webhook_url}`);

View File

@@ -33,7 +33,6 @@
"command-exists": "^1.2.9",
"compression": "^1.7.4",
"config": "^3.2.3",
"execa": "^5.1.1",
"express": "^4.18.2",
"express-session": "^1.17.3",
"feed": "^4.2.2",
@@ -62,10 +61,10 @@
"read-last-lines": "^1.7.2",
"rxjs": "^7.3.0",
"shortid": "^2.2.15",
"tree-kill": "^1.2.2",
"unzipper": "^0.10.10",
"uuidv4": "^6.2.13",
"winston": "^3.7.2",
"xmlbuilder2": "^3.0.2"
"xmlbuilder2": "^3.0.2",
"youtube-dl": "^3.0.2"
}
}

View File

@@ -1,7 +1,7 @@
const fs = require('fs-extra');
const path = require('path');
const youtubedl = require('youtube-dl');
const youtubedl_api = require('./youtube-dl');
const config_api = require('./config');
const archive_api = require('./archive');
const utils = require('./utils');
@@ -39,7 +39,7 @@ exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => {
exports.writeSubscriptionMetadata(sub);
if (success) {
if (!sub.paused) exports.getVideosForSub(sub.id);
if (!sub.paused) exports.getVideosForSub(sub, user_uid);
} else {
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
}
@@ -63,41 +63,55 @@ async function getSubscriptionInfo(sub) {
}
}
let {callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
const {parsed_output, err} = await callback;
if (err) {
logger.error(err.stderr);
return false;
}
logger.verbose('Subscribe: got info for subscription ' + sub.id);
for (const output_json of parsed_output) {
if (!output_json) {
continue;
}
if (!sub.name) {
if (sub.isPlaylist) {
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
} else {
sub.name = output_json.uploader;
return new Promise(async resolve => {
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async (err, output) => {
if (debugMode) {
logger.info('Subscribe: got info for subscription ' + sub.id);
}
// if it's now valid, update
if (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});
if (err) {
logger.error(err.stderr);
resolve(false);
} else if (output) {
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
logger.verbose('Could not get info for ' + sub.id);
resolve(false);
}
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
} catch(e) {
output_json = null;
}
if (!output_json) {
continue;
}
if (!sub.name) {
if (sub.isPlaylist) {
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
} else {
sub.name = output_json.uploader;
}
// if it's now valid, update
if (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});
}
}
// TODO: get even more info
resolve(true);
}
resolve(false);
}
}
return true;
}
return false;
});
});
}
exports.unsubscribe = async (sub_id, deleteMode, user_uid = null) => {
const sub = await exports.getSubscription(sub_id);
exports.unsubscribe = async (sub, deleteMode, user_uid = null) => {
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
@@ -120,7 +134,6 @@ exports.unsubscribe = async (sub_id, deleteMode, user_uid = null) => {
}
}
await killSubDownloads(sub_id, true);
await db_api.removeRecord('subscriptions', {id: id});
await db_api.removeAllRecords('files', {sub_id: id});
@@ -205,18 +218,12 @@ exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = nul
}
}
exports.getVideosForSub = async (sub_id) => {
const sub = await exports.getSubscription(sub_id);
if (!sub || sub['downloading']) {
exports.getVideosForSub = async (sub, user_uid = null) => {
const latest_sub_obj = await exports.getSubscription(sub.id);
if (!latest_sub_obj || latest_sub_obj['downloading']) {
return false;
}
_getVideosForSub(sub);
return true;
}
async function _getVideosForSub(sub) {
const user_uid = sub['user_uid'];
updateSubscriptionProperty(sub, {downloading: true}, user_uid);
// get basePath
@@ -234,26 +241,33 @@ async function _getVideosForSub(sub) {
// get videos
logger.verbose(`Subscription: getting list of videos to download for ${sub.name} with args: ${downloadConfig.join(',')}`);
let {child_process, callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
updateSubscriptionProperty(sub, {child_process: child_process}, user_uid);
const {parsed_output, err} = await callback;
updateSubscriptionProperty(sub, {downloading: false, child_process: null}, user_uid);
if (!parsed_output) {
logger.error('Subscription check failed!');
if (err) logger.error(err);
return null;
}
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);
}
// 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);
const files_to_download = await handleOutputJSON(parsed_output, sub, user_uid);
return files_to_download;
logger.verbose('Subscription: finished check for ' + sub.name);
const parsed_output = utils.parseOutputJSON(output, err);
if (!parsed_output) {
logger.error('Subscription check failed!');
resolve(null);
return;
}
const files_to_download = await handleOutputJSON(parsed_output, sub, user_uid);
resolve(files_to_download);
return;
});
}, err => {
logger.error(err);
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
});
}
async function handleOutputJSON(output_jsons, sub, user_uid) {
@@ -374,7 +388,7 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
downloadConfig.push('-r', rate_limit);
}
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
if (default_downloader === 'yt-dlp') {
downloadConfig.push('--no-clean-info-json');
}
@@ -405,37 +419,8 @@ async function getFilesToDownload(sub, output_jsons) {
return files_to_download;
}
exports.cancelCheckSubscription = async (sub_id) => {
const sub = await exports.getSubscription(sub_id);
if (!sub['downloading'] && !sub['child_process']) {
logger.error('Failed to cancel subscription check, verify that it is still running!');
return false;
}
// if check is ongoing
if (sub['child_process']) {
const child_process = sub['child_process'];
youtubedl_api.killYoutubeDLProcess(child_process);
}
// cancel activate video downloads
await killSubDownloads(sub_id);
return true;
}
async function killSubDownloads(sub_id, remove_downloads = false) {
const sub_downloads = await db_api.getRecords('download_queue', {sub_id: sub_id});
for (const sub_download of sub_downloads) {
if (sub_download['running'])
await downloader_api.cancelDownload(sub_download['uid']);
if (remove_downloads)
await db_api.removeRecord('download_queue', {uid: sub_download['uid']});
}
}
exports.getSubscriptions = async (user_uid = null) => {
// TODO: fix issue where the downloading property may not match getSubscription()
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
}
@@ -514,22 +499,24 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
const downloadConfig = await generateArgsForSubscription(sub, user_uid, true, new_path);
logger.verbose(`Checking if a better version of the fresh upload ${file_obj['id']} exists.`);
// simulate a download to verify that a better version exists
const info = await downloader_api.getVideoInfoByURL(file_obj['url'], downloadConfig);
if (info && info.length === 1) {
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
if (info[metric_to_compare] > file_obj[metric_to_compare]) {
// download new video as the simulated one is better
let {callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
const {parsed_output, err} = await callback;
if (err) {
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
} else if (parsed_output) {
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${info[metric_to_compare]}`);
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: info[metric_to_compare]});
}
}
}
youtubedl.getInfo(file_obj['url'], downloadConfig, async (err, output) => {
if (err) {
// video is not available anymore for whatever reason
} else if (output) {
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
if (output[metric_to_compare] > file_obj[metric_to_compare]) {
// download new video as the simulated one is better
youtubedl.exec(file_obj['url'], downloadConfig, {maxBuffer: Infinity}, async (err, output) => {
if (err) {
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
} else if (output) {
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${output[metric_to_compare]}`);
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]});
}
});
}
}
});
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false});
}

View File

@@ -4,9 +4,6 @@ const low = require('lowdb')
const winston = require('winston');
const path = require('path');
const util = require('util');
const fs = require('fs-extra');
const { uuid } = require('uuidv4');
const NodeID3 = require('node-id3');
const exec = util.promisify(require('child_process').exec);
const FileSync = require('lowdb/adapters/FileSync');
@@ -47,7 +44,9 @@ const categories_api = require('../categories');
const files_api = require('../files');
const youtubedl_api = require('../youtube-dl');
const config_api = require('../config');
const CONSTS = require('../consts');
const fs = require('fs-extra');
const { uuid } = require('uuidv4');
const NodeID3 = require('node-id3');
db_api.initialize(db, users_db, 'local_db_test.json');
@@ -402,7 +401,7 @@ describe('Multi User', async function() {
});
it('Subscription zip generator', async function() {
const sub = await subscriptions_api.getSubscription(sub_to_test.id, user_to_test);
const sub = await subscriptions_api.getSubscription(sub_to_test, user_to_test);
const sub_videos = await db_api.getRecords('files', {sub_id: sub.id});
assert(sub);
const sub_files_to_download = [];
@@ -442,34 +441,11 @@ describe('Multi User', async function() {
describe('Downloader', function() {
const downloader_api = require('../downloader');
const url = 'https://www.youtube.com/watch?v=hpigjnKl7nI';
const playlist_url = 'https://www.youtube.com/playlist?list=PLbZT16X07RLhqK-ZgSkRuUyiz9B_WLdNK';
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
const options = {
ui_uid: uuid()
}
async function createCategory(url) {
// get info
const args = await downloader_api.generateArgs(url, 'video', options, null, true);
const [info] = await downloader_api.getVideoInfoByURL(url, args);
// create category
await db_api.removeAllRecords('categories');
const new_category = {
name: 'test_category',
uid: uuid(),
rules: [],
custom_output: ''
};
await db_api.insertRecordIntoTable('categories', new_category);
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'includes',
property: 'title',
value: info['title']
});
}
before(async function() {
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
@@ -479,7 +455,6 @@ describe('Downloader', function() {
beforeEach(async function() {
// await db_api.connectToDB();
await db_api.removeAllRecords('download_queue');
config_api.setConfigItem('ytdl_allow_playlist_categorization', true);
});
it('Get file info', async function() {
@@ -505,32 +480,6 @@ describe('Downloader', function() {
assert(success);
});
it('Downloader - categorize', async function() {
this.timeout(300000);
await createCategory(url);
// collect info
const returned_download = await downloader_api.createDownload(url, 'video', options);
await downloader_api.collectInfo(returned_download['uid']);
assert(returned_download['category']);
assert(returned_download['category']['name'] === 'test_category');
});
it('Downloader - categorize playlist', async function() {
this.timeout(300000);
await createCategory(playlist_url);
// collect info
const returned_download_pass = await downloader_api.createDownload(playlist_url, 'video', options);
await downloader_api.collectInfo(returned_download_pass['uid']);
assert(returned_download_pass['category']);
assert(returned_download_pass['category']['name'] === 'test_category');
// test with playlist categorization disabled
config_api.setConfigItem('ytdl_allow_playlist_categorization', false);
const returned_download_fail = await downloader_api.createDownload(playlist_url, 'video', options);
await downloader_api.collectInfo(returned_download_fail['uid']);
assert(!returned_download_fail['category']);
});
it('Tag file', async function() {
const success = await generateEmptyAudioFile('test/sample_mp3.mp3');
const audio_path = './test/sample_mp3.mp3';
@@ -603,7 +552,7 @@ describe('Downloader', function() {
});
describe('Twitch', async function () {
const twitch_api = require('../twitch');
const example_vod = '1790315420';
const example_vod = '1710641401';
it('Download VOD chat', async function() {
this.timeout(300000);
if (!fs.existsSync('TwitchDownloaderCLI')) {
@@ -625,105 +574,6 @@ describe('Downloader', function() {
});
});
describe('youtube-dl', async function() {
beforeEach(async function () {
if (fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.unlinkSync(CONSTS.DETAILS_BIN_PATH);
await youtubedl_api.checkForYoutubeDLUpdate();
});
it('Check latest version', async function() {
this.timeout(300000);
const original_fork = config_api.getConfigItem('ytdl_default_downloader');
const latest_version = await youtubedl_api.getLatestUpdateVersion(original_fork);
assert(latest_version > CONSTS.OUTDATED_YOUTUBEDL_VERSION);
});
it('Update youtube-dl', async function() {
this.timeout(300000);
const original_fork = config_api.getConfigItem('ytdl_default_downloader');
const binary_path = path.join('test', 'test_binary');
for (const youtubedl_fork in youtubedl_api.youtubedl_forks) {
config_api.setConfigItem('ytdl_default_downloader', youtubedl_fork);
const latest_version = await youtubedl_api.checkForYoutubeDLUpdate();
await youtubedl_api.updateYoutubeDL(latest_version, binary_path);
assert(fs.existsSync(binary_path));
if (fs.existsSync(binary_path)) fs.unlinkSync(binary_path);
}
config_api.setConfigItem('ytdl_default_downloader', original_fork);
});
it('Run process', async function() {
this.timeout(300000);
const downloader_api = require('../downloader');
const url = 'https://www.youtube.com/watch?v=hpigjnKl7nI';
const args = await downloader_api.generateArgs(url, 'video', {}, null, true);
const {child_process} = await youtubedl_api.runYoutubeDL(url, args);
assert(child_process);
});
});
describe('Subscriptions', function() {
const new_sub = {
name: 'test_sub',
url: 'https://www.youtube.com/channel/UCzofo-P8yMMCOv8rsPfIR-g',
maxQuality: null,
id: uuid(),
user_uid: null,
type: 'video',
paused: true
};
beforeEach(async function() {
await db_api.removeAllRecords('subscriptions');
});
it('Subscribe', async function () {
const success = await subscriptions_api.subscribe(new_sub, null, true);
assert(success);
const sub_exists = await db_api.getRecord('subscriptions', {id: new_sub['id']});
assert(sub_exists);
});
it('Unsubscribe', async function () {
await subscriptions_api.subscribe(new_sub, null, true);
await subscriptions_api.unsubscribe(new_sub);
const sub_exists = await db_api.getRecord('subscriptions', {id: new_sub['id']});
assert(!sub_exists);
});
it('Delete subscription file', async function () {
});
it('Get subscription by name', async function () {
await subscriptions_api.subscribe(new_sub, null, true);
const sub_by_name = await subscriptions_api.getSubscriptionByName('test_sub');
assert(sub_by_name);
});
it('Get subscriptions', async function() {
await subscriptions_api.subscribe(new_sub, null, true);
const subs = await subscriptions_api.getSubscriptions(null);
assert(subs && subs.length === 1);
});
it('Update subscription', async function () {
await subscriptions_api.subscribe(new_sub, null, true);
const sub_update = Object.assign({}, new_sub, {name: 'updated_name'});
await subscriptions_api.updateSubscription(sub_update);
const updated_sub = await db_api.getRecord('subscriptions', {id: new_sub['id']});
assert(updated_sub['name'] === 'updated_name');
});
it('Update subscription property', async function () {
await subscriptions_api.subscribe(new_sub, null, true);
const sub_update = Object.assign({}, new_sub, {name: 'updated_name'});
await subscriptions_api.updateSubscriptionPropertyMultiple([sub_update], {name: 'updated_name'});
const updated_sub = await db_api.getRecord('subscriptions', {id: new_sub['id']});
assert(updated_sub['name'] === 'updated_name');
});
it('Write subscription metadata', async function() {
const metadata_path = path.join('subscriptions', 'channels', 'test_sub', 'subscription_backup.json');
if (fs.existsSync(metadata_path)) fs.unlinkSync(metadata_path);
await subscriptions_api.subscribe(new_sub, null, true);
assert(fs.existsSync(metadata_path));
});
it('Fresh uploads', async function() {
});
});
describe('Tasks', function() {
const tasks_api = require('../tasks');
beforeEach(async function() {
@@ -785,7 +635,7 @@ describe('Tasks', function() {
const success = await generateEmptyVideoFile('test/sample_mp4.mp4');
// pre-test cleanup
await db_api.removeAllRecords('files', {path: 'test/missing_file.mp4'});
await db_api.removeAllRecords('files', {title: 'Sample File'});
if (fs.existsSync('video/sample_mp4.info.json')) fs.unlinkSync('video/sample_mp4.info.json');
if (fs.existsSync('video/sample_mp4.mp4')) fs.unlinkSync('video/sample_mp4.mp4');
@@ -942,7 +792,7 @@ describe('Categories', async function() {
rules: [],
custom_output: ''
};
await db_api.removeAllRecords('categories', {name: 'test_category'});
await db_api.insertRecordIntoTable('categories', new_category);
});
@@ -1037,66 +887,6 @@ describe('Categories', async function() {
});
});
describe('Config', async function() {
it('findChangedConfigItems', async function() {
const old_config = {
"YoutubeDLMaterial": {
"test_object1": {
"test_prop1": true,
"test_prop2": false
},
"test_object2": {
"test_prop3": {
"test_prop3_1": true,
"test_prop3_2": false
},
"test_prop4": false
},
"test_object3": {
"test_prop5": {
"test_prop5_1": true,
"test_prop5_2": false
},
"test_prop6": false
}
}
};
const new_config = {
"YoutubeDLMaterial": {
"test_object1": {
"test_prop1": false,
"test_prop2": false
},
"test_object2": {
"test_prop3": {
"test_prop3_1": false,
"test_prop3_2": false
},
"test_prop4": true
},
"test_object3": {
"test_prop5": {
"test_prop5_1": true,
"test_prop5_2": false
},
"test_prop6": true
}
}
};
const changes = config_api.findChangedConfigItems(old_config, new_config);
assert(changes[0]['key'] === 'test_prop1' && changes[0]['old_value'] === true && changes[0]['new_value'] === false);
assert(changes[1]['key'] === 'test_prop3' &&
changes[1]['old_value']['test_prop3_1'] === true &&
changes[1]['new_value']['test_prop3_1'] === false &&
changes[1]['old_value']['test_prop3_2'] === false &&
changes[1]['new_value']['test_prop3_2'] === false);
assert(changes[2]['key'] === 'test_prop4' && changes[2]['old_value'] === false && changes[2]['new_value'] === true);
assert(changes[3]['key'] === 'test_prop6' && changes[3]['old_value'] === false && changes[3]['new_value'] === true);
});
});
const generateEmptyVideoFile = async (file_path) => {
if (fs.existsSync(file_path)) fs.unlinkSync(file_path);
return await exec(`ffmpeg -t 1 -f lavfi -i color=c=black:s=640x480 -c:v libx264 -tune stillimage -pix_fmt yuv420p "${file_path}"`);

View File

@@ -241,6 +241,11 @@ exports.addUIDsToCategory = (category, files) => {
return files_that_match;
}
exports.getCurrentDownloader = () => {
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
return details_json['downloader'];
}
exports.recFindByExt = async (base, ext, files, result, recursive = true) => {
files = files || (await fs.readdir(base))
result = result || []

View File

@@ -1,159 +1,151 @@
const fs = require('fs-extra');
const fetch = require('node-fetch');
const path = require('path');
const execa = require('execa');
const kill = require('tree-kill');
const logger = require('./logger');
const utils = require('./utils');
const CONSTS = require('./consts');
const config_api = require('./config.js');
const youtubedl = require('youtube-dl');
const OUTDATED_VERSION = "2020.00.00";
const is_windows = process.platform === 'win32';
exports.youtubedl_forks = {
const download_sources = {
'youtube-dl': {
'download_url': 'https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl',
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags'
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags',
'func': downloadLatestYoutubeDLBinary
},
'youtube-dlc': {
'download_url': 'https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc',
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags'
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags',
'func': downloadLatestYoutubeDLCBinary
},
'yt-dlp': {
'download_url': 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp',
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags'
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags',
'func': downloadLatestYoutubeDLPBinary
}
}
exports.runYoutubeDL = async (url, args, customDownloadHandler = null) => {
const output_file_path = getYoutubeDLPath();
if (!fs.existsSync(output_file_path)) await exports.checkForYoutubeDLUpdate();
let callback = null;
let child_process = null;
if (customDownloadHandler) {
callback = runYoutubeDLCustom(url, args, customDownloadHandler);
} else {
({callback, child_process} = await runYoutubeDLProcess(url, args));
}
return {child_process, callback};
}
// Run youtube-dl directly (not cancellable)
const runYoutubeDLCustom = async (url, args, customDownloadHandler) => {
const downloadHandler = customDownloadHandler;
exports.runYoutubeDL = async (url, args, downloadMethod = youtubedl.exec) => {
return new Promise(resolve => {
downloadHandler(url, args, {maxBuffer: Infinity}, async function(err, output) {
downloadMethod(url, args, {maxBuffer: Infinity}, async function(err, output) {
const parsed_output = utils.parseOutputJSON(output, err);
resolve({parsed_output, err});
});
});
}
// Run youtube-dl in a subprocess (cancellable)
const runYoutubeDLProcess = async (url, args, youtubedl_fork = config_api.getConfigItem('ytdl_default_downloader')) => {
const youtubedl_path = getYoutubeDLPath(youtubedl_fork);
const binary_exists = fs.existsSync(youtubedl_path);
if (!binary_exists) {
const err = `Could not find path for ${youtubedl_fork} at ${youtubedl_path}`;
logger.error(err);
return;
}
const child_process = execa(getYoutubeDLPath(youtubedl_fork), [url, ...args], {maxBuffer: Infinity});
const callback = new Promise(async resolve => {
try {
const {stdout, stderr} = await child_process;
const parsed_output = utils.parseOutputJSON(stdout.trim().split(/\r?\n/), stderr);
resolve({parsed_output, err: stderr});
} catch (e) {
resolve({parsed_output: null, err: e})
}
});
return {child_process, callback}
}
function getYoutubeDLPath(youtubedl_fork = config_api.getConfigItem('ytdl_default_downloader')) {
const binary_file_name = youtubedl_fork + (is_windows ? '.exe' : '');
const binary_path = path.join('appdata', 'bin', binary_file_name);
return binary_path;
}
exports.killYoutubeDLProcess = async (child_process) => {
kill(child_process.pid, 'SIGKILL');
}
exports.checkForYoutubeDLUpdate = async () => {
const selected_fork = config_api.getConfigItem('ytdl_default_downloader');
const output_file_path = getYoutubeDLPath();
// get current version
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
if (!current_app_details_exists) {
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
updateDetailsJSON(CONSTS.OUTDATED_YOUTUBEDL_VERSION, selected_fork, output_file_path);
}
const current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
const current_version = current_app_details['version'];
const current_fork = current_app_details['downloader'];
return new Promise(async resolve => {
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
const tags_url = download_sources[default_downloader]['tags_url'];
// get current version
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
if (!current_app_details_exists) {
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version": OUTDATED_VERSION, "downloader": default_downloader});
}
let current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
let current_version = current_app_details['version'];
let current_downloader = current_app_details['downloader'];
let stored_binary_path = current_app_details['path'];
if (!stored_binary_path || typeof stored_binary_path !== 'string') {
// logger.info(`INFO: Failed to get youtube-dl binary path at location: ${CONSTS.DETAILS_BIN_PATH}, attempting to guess actual path...`);
const guessed_base_path = 'node_modules/youtube-dl/bin/';
const guessed_file_path = guessed_base_path + 'youtube-dl' + (is_windows ? '.exe' : '');
if (fs.existsSync(guessed_file_path)) {
stored_binary_path = guessed_file_path;
// logger.info('INFO: Guess successful! Update process continuing...')
} else {
logger.error(`Guess '${guessed_file_path}' is not correct. Cancelling update check. Verify that your youtube-dl binaries exist by running npm install.`);
resolve(null);
return;
}
}
const latest_version = await exports.getLatestUpdateVersion(selected_fork);
// if the binary does not exist, or default_downloader doesn't match existing fork, or if the fork has been updated, redownload
// TODO: don't redownload if fork already exists
if (!fs.existsSync(output_file_path) || current_fork !== selected_fork || !current_version || current_version !== latest_version) {
logger.warn(`Updating ${selected_fork} binary to '${output_file_path}', downloading...`);
await exports.updateYoutubeDL(latest_version);
}
}
// got version, now let's check the latest version from the youtube-dl API
exports.updateYoutubeDL = async (latest_update_version, custom_output_path = null) => {
await fs.ensureDir(path.join('appdata', 'bin'));
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
await downloadLatestYoutubeDLBinaryGeneric(default_downloader, latest_update_version, custom_output_path);
}
async function downloadLatestYoutubeDLBinaryGeneric(youtubedl_fork, new_version, custom_output_path = null) {
const file_ext = is_windows ? '.exe' : '';
// build the URL
const download_url = `${exports.youtubedl_forks[youtubedl_fork]['download_url']}${file_ext}`;
const output_path = custom_output_path || getYoutubeDLPath(youtubedl_fork);
await utils.fetchFile(download_url, output_path, `${youtubedl_fork} ${new_version}`);
fs.chmod(output_path, 0o777);
updateDetailsJSON(new_version, youtubedl_fork, output_path);
}
exports.getLatestUpdateVersion = async (youtubedl_fork) => {
const tags_url = exports.youtubedl_forks[youtubedl_fork]['tags_url'];
return new Promise(resolve => {
fetch(tags_url, {method: 'Get'})
.then(async res => res.json())
.then(async (json) => {
// check if the versions are different
if (!json || !json[0]) {
logger.error(`Failed to check ${youtubedl_fork} version for an update.`)
logger.error(`Failed to check ${default_downloader} version for an update.`)
resolve(null);
return;
}
const latest_update_version = json[0]['name'];
resolve(latest_update_version);
if (current_version !== latest_update_version || default_downloader !== current_downloader) {
// versions different or different downloader is being used, download new update
resolve(latest_update_version);
} else {
resolve(null);
}
return;
})
.catch(err => {
logger.error(`Failed to check ${youtubedl_fork} version for an update.`)
logger.error(`Failed to check ${default_downloader} version for an update.`)
logger.error(err);
resolve(null);
return;
});
});
}
function updateDetailsJSON(new_version, fork, output_path) {
exports.updateYoutubeDL = async (latest_update_version) => {
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
await download_sources[default_downloader]['func'](latest_update_version);
}
exports.verifyBinaryExistsLinux = () => {
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
if (!is_windows && details_json && (!details_json['path'] || details_json['path'].includes('.exe'))) {
details_json['path'] = 'node_modules/youtube-dl/bin/youtube-dl';
details_json['exec'] = 'youtube-dl';
details_json['version'] = OUTDATED_VERSION;
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
utils.restartServer();
}
}
async function downloadLatestYoutubeDLBinary(new_version) {
const file_ext = is_windows ? '.exe' : '';
const details_json = fs.existsSync(CONSTS.DETAILS_BIN_PATH) ? fs.readJSONSync(CONSTS.DETAILS_BIN_PATH) : {};
if (!details_json[fork]) details_json[fork] = {};
const fork_json = details_json[fork];
fork_json['version'] = new_version;
fork_json['downloader'] = fork;
fork_json['path'] = output_path; // unused
fork_json['exec'] = fork + file_ext; // unused
const download_url = `https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl${file_ext}`;
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
await utils.fetchFile(download_url, output_path, `youtube-dl ${new_version}`);
updateDetailsJSON(new_version, 'youtube-dl');
}
async function downloadLatestYoutubeDLCBinary(new_version) {
const file_ext = is_windows ? '.exe' : '';
const download_url = `https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc${file_ext}`;
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
await utils.fetchFile(download_url, output_path, `youtube-dlc ${new_version}`);
updateDetailsJSON(new_version, 'youtube-dlc');
}
async function downloadLatestYoutubeDLPBinary(new_version) {
const file_ext = is_windows ? '.exe' : '';
const download_url = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp${file_ext}`;
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
await utils.fetchFile(download_url, output_path, `yt-dlp ${new_version}`);
updateDetailsJSON(new_version, 'yt-dlp');
}
function updateDetailsJSON(new_version, downloader) {
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
if (new_version) details_json['version'] = new_version;
details_json['downloader'] = downloader;
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
}

8
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.3.1",
"version": "4.3.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -11647,9 +11647,9 @@
}
},
"socket.io-parser": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz",
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==",
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",

View File

@@ -14,7 +14,6 @@ export type { ChangeRolePermissionsRequest } from './models/ChangeRolePermission
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
export type { CheckSubscriptionRequest } from './models/CheckSubscriptionRequest';
export type { ClearDownloadsRequest } from './models/ClearDownloadsRequest';
export type { ConcurrentStream } from './models/ConcurrentStream';
export type { Config } from './models/Config';

View File

@@ -1,7 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CheckSubscriptionRequest = {
sub_id: string;
};

View File

@@ -8,7 +8,6 @@ export type Download = {
running: boolean;
finished: boolean;
paused: boolean;
cancelled?: boolean;
finished_step: boolean;
url: string;
type: string;

View File

@@ -11,12 +11,9 @@ export type Subscription = {
type: FileType;
user_uid: string | null;
isPlaylist: boolean;
child_process?: any;
archive?: string;
timerange?: string;
custom_args?: string;
custom_output?: string;
downloading?: boolean;
paused?: boolean;
videos: Array<any>;
};

View File

@@ -2,8 +2,10 @@
/* tslint:disable */
/* eslint-disable */
import type { SubscriptionRequestData } from './SubscriptionRequestData';
export type UnsubscribeRequest = {
sub_id: string;
sub: SubscriptionRequestData;
/**
* Defaults to false
*/

View File

@@ -69,7 +69,8 @@ export class DownloadsComponent implements OnInit, OnDestroy {
tooltip: $localize`Pause`,
action: (download: Download) => this.pauseDownload(download),
show: (download: Download) => !download.finished && (!download.paused || !download.finished_step),
icon: 'pause'
icon: 'pause',
loading: (download: Download) => download.paused && !download.finished_step
},
{
tooltip: $localize`Resume`,
@@ -80,7 +81,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
{
tooltip: $localize`Cancel`,
action: (download: Download) => this.cancelDownload(download),
show: (download: Download) => !download.finished && !download.paused && !download.cancelled,
show: (download: Download) => false && !download.finished && !download.paused, // TODO: add possibility to cancel download
icon: 'cancel'
},
{

View File

@@ -2,7 +2,6 @@ import { Component, OnInit, Inject } from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { PostsService } from 'app/posts.services';
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component';
import { Subscription } from 'api-types';
@Component({
selector: 'app-subscription-info-dialog',
@@ -11,7 +10,7 @@ import { Subscription } from 'api-types';
})
export class SubscriptionInfoDialogComponent implements OnInit {
sub: Subscription = null;
sub = null;
unsubbedEmitter = null;
constructor(public dialogRef: MatDialogRef<SubscriptionInfoDialogComponent>,
@@ -42,7 +41,7 @@ export class SubscriptionInfoDialogComponent implements OnInit {
}
unsubscribe() {
this.postsService.unsubscribe(this.sub.id, true).subscribe(res => {
this.postsService.unsubscribe(this.sub, true).subscribe(res => {
this.unsubbedEmitter.emit(true);
this.dialogRef.close();
});

View File

@@ -169,8 +169,6 @@ export class MainComponent implements OnInit {
argsChangedSubject: Subject<boolean> = new Subject<boolean>();
simulatedOutput = '';
interval_id = null;
constructor(public postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
private router: Router, public dialog: MatDialog, private platform: Platform, private route: ActivatedRoute) {
this.audioOnly = false;
@@ -234,12 +232,11 @@ export class MainComponent implements OnInit {
}
// get downloads routine
if (this.interval_id) { clearInterval(this.interval_id) }
this.interval_id = setInterval(() => {
setInterval(() => {
if (this.current_download) {
this.getCurrentDownload();
}
}, 1000);
}, 500);
return true;
}
@@ -297,10 +294,6 @@ export class MainComponent implements OnInit {
}
}
ngOnDestroy(): void {
if (this.interval_id) { clearInterval(this.interval_id) }
}
// download helpers
downloadHelper(container: DatabaseFile | Playlist, type: string, is_playlist = false, force_view = false, navigate_mode = false): void {
this.downloadingfile = false;

View File

@@ -113,8 +113,7 @@ import {
Archive,
Subscription,
RestartDownloadResponse,
TaskType,
CheckSubscriptionRequest
TaskType
} from '../api-types';
import { isoLangs } from './dialogs/user-profile-dialog/locales_list';
import { Title } from '@angular/platform-browser';
@@ -567,18 +566,8 @@ export class PostsService implements CanActivate {
return this.http.post<SuccessObject>(this.path + 'updateSubscription', {subscription: subscription}, this.httpOptions);
}
checkSubscription(sub_id: string) {
const body: CheckSubscriptionRequest = {sub_id: sub_id};
return this.http.post<SuccessObject>(this.path + 'checkSubscription', body, this.httpOptions);
}
cancelCheckSubscription(sub_id: string) {
const body: CheckSubscriptionRequest = {sub_id: sub_id};
return this.http.post<SuccessObject>(this.path + 'cancelCheckSubscription', body, this.httpOptions);
}
unsubscribe(sub_id: string, deleteMode = false) {
const body: UnsubscribeRequest = {sub_id: sub_id, deleteMode: deleteMode};
unsubscribe(sub: SubscriptionRequestData, deleteMode = false) {
const body: UnsubscribeRequest = {sub: sub, deleteMode: deleteMode};
return this.http.post<UnsubscribeResponse>(this.path + 'unsubscribe', body, this.httpOptions)
}

View File

@@ -426,13 +426,6 @@
<mat-hint><a target="_blank" href="https://stackoverflow.com/a/37396871/8088021"><ng-container i18n="Telegram chat ID help">How do I get the chat ID?</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mb-2">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="Telegram webhook proxy">Telegram webhook proxy</mat-label>
<input placeholder="https://smee.io/XXXXX" [disabled]="!new_config['Extra']['enable_notifications'] || !new_config['API']['use_telegram_API']" [(ngModel)]="new_config['API']['telegram_webhook_proxy']" matInput>
<mat-hint><a target="_blank" href="https://smee.io/"><ng-container i18n="Telegram webhook proxy help">Example service</ng-container></a></mat-hint>
</mat-form-field>
</div>
</div>
</div>
</ng-template>

View File

@@ -3,7 +3,6 @@
<div style="margin-bottom: 15px;">
<h2 style="text-align: center;" *ngIf="subscription">
{{subscription.name}}&nbsp;<ng-container *ngIf="subscription.paused" i18n="Paused suffix">(Paused)</ng-container>
<button class="edit-button" (click)="editSubscription()" [disabled]="downloading" matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button><mat-icon class="save-icon">edit</mat-icon></button>
</h2>
<mat-progress-bar style="width: 80%; margin: 0 auto; margin-top: 15px;" *ngIf="subscription && subscription.downloading" mode="indeterminate"></mat-progress-bar>
</div>
@@ -14,14 +13,7 @@
<div style="margin-bottom: 100px;" *ngIf="subscription">
<app-recent-videos #recentVideos [sub_id]="subscription.id"></app-recent-videos>
</div>
<div class="check-button">
<ng-container *ngIf="subscription.downloading">
<button color="primary" (click)="cancelCheckSubscription()" [disabled]="cancel_clicked" matTooltip="Cancel subscription check" i18n-matTooltip="Cancel subscription check" mat-fab><mat-icon class="save-icon">cancel</mat-icon></button>
</ng-container>
<ng-container *ngIf="!subscription.downloading">
<button color="primary" (click)="checkSubscription()" [disabled]="check_clicked" matTooltip="Check subscription" i18n-matTooltip="Check subscription" mat-fab><mat-icon class="save-icon">youtube_searched_for</mat-icon></button>
</ng-container>
</div>
<button class="edit-button" color="primary" (click)="editSubscription()" [disabled]="downloading" matTooltip="Edit" i18n-matTooltip="Edit" mat-fab><mat-icon class="save-icon">edit</mat-icon></button>
<button class="watch-button" color="primary" (click)="watchSubscription()" matTooltip="Play all" i18n-matTooltip="Play all" mat-fab><mat-icon class="save-icon">video_library</mat-icon></button>
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" matTooltip="Download zip" i18n-matTooltip="Download zip" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
</div>

View File

@@ -58,19 +58,13 @@
bottom: 25px;
}
.check-button {
.edit-button {
left: 25px;
position: fixed;
bottom: 25px;
z-index: 99999;
}
.edit-button {
right: 35px;
position: fixed;
z-index: 99999;
}
.save-icon {
bottom: 1px;
position: relative;

View File

@@ -3,7 +3,6 @@ import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
import { Subscription } from 'api-types';
@Component({
selector: 'app-subscription',
@@ -13,13 +12,11 @@ import { Subscription } from 'api-types';
export class SubscriptionComponent implements OnInit, OnDestroy {
id = null;
subscription: Subscription = null;
subscription = null;
use_youtubedl_archive = false;
descendingMode = true;
downloading = false;
sub_interval = null;
check_clicked = false;
cancel_clicked = false;
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { }
@@ -93,34 +90,4 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
this.router.navigate(['/player', {sub_id: this.subscription.id}])
}
checkSubscription(): void {
this.check_clicked = true;
this.postsService.checkSubscription(this.subscription.id).subscribe(res => {
this.check_clicked = false;
if (!res['success']) {
this.postsService.openSnackBar('Failed to check subscription!');
return;
}
}, err => {
console.error(err);
this.check_clicked = false;
this.postsService.openSnackBar('Failed to check subscription!');
});
}
cancelCheckSubscription(): void {
this.cancel_clicked = true;
this.postsService.cancelCheckSubscription(this.subscription.id).subscribe(res => {
this.cancel_clicked = false;
if (!res['success']) {
this.postsService.openSnackBar('Failed to cancel check subscription!');
return;
}
}, err => {
console.error(err);
this.cancel_clicked = false;
this.postsService.openSnackBar('Failed to cancel check subscription!');
});
}
}