mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-04-29 18:03:20 +03:00
Download manager is now functional
Added UI support for new downloads schema Implemented draft test for downloads Cleaned up unused code snippets
This commit is contained in:
@@ -30,10 +30,11 @@ if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN
|
|||||||
const youtubedl = require('youtube-dl');
|
const youtubedl = require('youtube-dl');
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
var config_api = require('./config.js');
|
const config_api = require('./config.js');
|
||||||
var subscriptions_api = require('./subscriptions')
|
const downloader_api = require('./downloader');
|
||||||
var categories_api = require('./categories');
|
const subscriptions_api = require('./subscriptions');
|
||||||
var twitch_api = require('./twitch');
|
const categories_api = require('./categories');
|
||||||
|
const twitch_api = require('./twitch');
|
||||||
|
|
||||||
const is_windows = process.platform === 'win32';
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ const admin_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
|||||||
config_api.initialize();
|
config_api.initialize();
|
||||||
db_api.initialize(db, users_db);
|
db_api.initialize(db, users_db);
|
||||||
auth_api.initialize(db_api);
|
auth_api.initialize(db_api);
|
||||||
|
downloader_api.initialize(db_api);
|
||||||
subscriptions_api.initialize(db_api);
|
subscriptions_api.initialize(db_api);
|
||||||
categories_api.initialize(db_api);
|
categories_api.initialize(db_api);
|
||||||
|
|
||||||
@@ -1479,9 +1481,10 @@ app.post('/api/downloadFile', optionalJwt, async function(req, res) {
|
|||||||
cropFileSettings: req.body.cropFileSettings
|
cropFileSettings: req.body.cropFileSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
let result_obj = await downloadFileByURL_exec(url, type, options, req.query.sessionID);
|
const download = await downloader_api.createDownload(url, type, options);
|
||||||
if (result_obj) {
|
|
||||||
res.send(result_obj);
|
if (download) {
|
||||||
|
res.send({download: download});
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(500);
|
res.sendStatus(500);
|
||||||
}
|
}
|
||||||
@@ -2294,18 +2297,12 @@ app.get('/api/thumbnail/:path', optionalJwt, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/download', async (req, res) => {
|
app.post('/api/download', async (req, res) => {
|
||||||
const session_id = req.body.session_id;
|
const download_uid = req.body.download_uid;
|
||||||
const download_id = req.body.download_id;
|
|
||||||
const session_downloads = downloads.find(potential_session_downloads => potential_session_downloads['session_id'] === session_id);
|
|
||||||
let found_download = null;
|
|
||||||
|
|
||||||
// find download
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
if (session_downloads && Object.keys(session_downloads)) {
|
|
||||||
found_download = Object.values(session_downloads).find(session_download => session_download['ui_uid'] === download_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found_download) {
|
if (download) {
|
||||||
res.send({download: found_download});
|
res.send({download: download});
|
||||||
} else {
|
} else {
|
||||||
res.send({download: null});
|
res.send({download: null});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ exports.registerUser = async function(req, res) {
|
|||||||
|
|
||||||
exports.login = async (username, password) => {
|
exports.login = async (username, password) => {
|
||||||
const user = await db_api.getRecord('users', {name: username});
|
const user = await db_api.getRecord('users', {name: username});
|
||||||
if (!user) { logger.error(`User ${username} not found`); false }
|
if (!user) { logger.error(`User ${username} not found`); return false }
|
||||||
if (user.auth_method && user.auth_method !== 'internal') { return false }
|
if (user.auth_method && user.auth_method !== 'internal') { return false }
|
||||||
return await bcrypt.compare(password, user.passhash) ? user : false;
|
return await bcrypt.compare(password, user.passhash) ? user : false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ const tables = {
|
|||||||
name: 'roles',
|
name: 'roles',
|
||||||
primary_key: 'key'
|
primary_key: 'key'
|
||||||
},
|
},
|
||||||
|
download_queue: {
|
||||||
|
name: 'download_queue',
|
||||||
|
primary_key: 'uid'
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
name: 'test'
|
name: 'test'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,114 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const queue = require('queue');
|
const mergeFiles = require('merge-files');
|
||||||
|
const NodeID3 = require('node-id3')
|
||||||
|
const glob = require("glob")
|
||||||
|
|
||||||
const youtubedl = require('youtube-dl');
|
const youtubedl = require('youtube-dl');
|
||||||
|
|
||||||
|
const logger = require('./logger');
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const twitch_api = require('./twitch');
|
const twitch_api = require('./twitch');
|
||||||
|
const categories_api = require('./categories');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
|
|
||||||
let db_api = null;
|
let db_api = null;
|
||||||
let logger = null;
|
|
||||||
|
const STEP_INDEX_TO_LABEL = {
|
||||||
|
0: 'Creating download',
|
||||||
|
1: 'Getting info',
|
||||||
|
2: 'Downloading file'
|
||||||
|
}
|
||||||
|
|
||||||
|
const archivePath = path.join(__dirname, 'appdata', 'archives');
|
||||||
|
|
||||||
function setDB(input_db_api) { db_api = input_db_api }
|
function setDB(input_db_api) { db_api = input_db_api }
|
||||||
function setLogger(input_logger) { logger = input_logger; }
|
|
||||||
|
|
||||||
exports.initialize = (input_db_api, input_logger) => {
|
exports.initialize = (input_db_api) => {
|
||||||
setDB(input_db_api);
|
setDB(input_db_api);
|
||||||
setLogger(input_logger);
|
setInterval(checkDownloads, 10000);
|
||||||
|
categories_api.initialize(db_api);
|
||||||
|
// temporary
|
||||||
|
db_api.removeAllRecords('download_queue');
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createDownload = async (url, type, options) => {
|
||||||
|
const download = {
|
||||||
|
url: url,
|
||||||
|
type: type,
|
||||||
|
options: options,
|
||||||
|
uid: uuid(),
|
||||||
|
step_index: 0,
|
||||||
|
paused: false,
|
||||||
|
finished_step: true,
|
||||||
|
error: null,
|
||||||
|
percent_complete: null,
|
||||||
|
finished: false,
|
||||||
|
timestamp_start: Date.now()
|
||||||
|
};
|
||||||
|
await db_api.insertRecordIntoTable('download_queue', download);
|
||||||
|
return download;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.pauseDownload = () => {
|
exports.pauseDownload = () => {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// questions
|
||||||
|
// how do we want to manage queued downloads that errored in any step? do we set the index back and finished_step to true or let the manager do it?
|
||||||
|
|
||||||
async function checkDownloads() {
|
async function checkDownloads() {
|
||||||
|
logger.verbose('Checking downloads');
|
||||||
const downloads = await db_api.getRecords('download_queue');
|
const downloads = await db_api.getRecords('download_queue');
|
||||||
downloads.sort((download1, download2) => download1.timestamp_start - download2.timestamp_start);
|
downloads.sort((download1, download2) => download1.timestamp_start - download2.timestamp_start);
|
||||||
downloads = downloads.filter(download => !download.paused);
|
const running_downloads = downloads.filter(download => !download.paused);
|
||||||
for (let i = 0; i < downloads.length; i++) {
|
for (let i = 0; i < running_downloads.length; i++) {
|
||||||
if (i === config_api.getConfigItem('ytdl_'))
|
const running_download = running_downloads[i];
|
||||||
|
if (i === 5/*config_api.getConfigItem('ytdl_max_concurrent_downloads')*/) break;
|
||||||
|
|
||||||
|
if (running_download['finished_step'] && !running_download['finished']) {
|
||||||
|
// move to next step
|
||||||
|
|
||||||
|
if (running_download['step_index'] === 0) {
|
||||||
|
|
||||||
|
collectInfo(running_download['uid']);
|
||||||
|
} else if (running_download['step_index'] === 1) {
|
||||||
|
downloadQueuedFile(running_download['uid']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createDownload(url, type, options) {
|
|
||||||
const download = {url: url, type: type, options: options, uid: uuid()};
|
|
||||||
await db_api.insertRecord(download);
|
|
||||||
return download;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectInfo(download_uid) {
|
async function collectInfo(download_uid) {
|
||||||
const download = db_api.getRecord('download_queue', {uid: download_uid});
|
logger.verbose(`Collecting info for download ${download_uid}`);
|
||||||
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 1, finished_step: false});
|
||||||
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
|
||||||
const url = download['url'];
|
const url = download['url'];
|
||||||
const type = download['type'];
|
const type = download['type'];
|
||||||
const options = download['options'];
|
const options = download['options'];
|
||||||
const args = download['args'];
|
|
||||||
|
if (options.user && !options.customFileFolderPath) {
|
||||||
|
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
|
const user_path = path.join(usersFileFolder, options.user, type);
|
||||||
|
options.customFileFolderPath = user_path + path.sep;
|
||||||
|
}
|
||||||
|
|
||||||
|
let args = await generateArgs(url, type, options);
|
||||||
|
|
||||||
// get video info prior to download
|
// get video info prior to download
|
||||||
const info = await getVideoInfoByURL(url, args);
|
let info = await getVideoInfoByURL(url, args, download_uid);
|
||||||
|
|
||||||
if (!info) {
|
if (!info) {
|
||||||
// info failed, record error and pause download
|
// info failed, record error and pause download
|
||||||
|
const error = 'Failed to get info, see server logs for specific error.';
|
||||||
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {error: error, paused: true});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let category = null;
|
||||||
|
|
||||||
// check if it fits into a category. If so, then get info again using new args
|
// check if it fits into a category. If so, then get info again using new args
|
||||||
if (!Array.isArray(info) || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
if (!Array.isArray(info) || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
||||||
|
|
||||||
@@ -61,22 +117,37 @@ async function collectInfo(download_uid) {
|
|||||||
options.customOutput = category['custom_output'];
|
options.customOutput = category['custom_output'];
|
||||||
options.noRelativePath = true;
|
options.noRelativePath = true;
|
||||||
args = await generateArgs(url, type, options);
|
args = await generateArgs(url, type, options);
|
||||||
info = await getVideoInfoByURL(url, args);
|
info = await getVideoInfoByURL(url, args, download_uid);
|
||||||
|
|
||||||
// must update args
|
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {remote_metadata: info});
|
// setup info required to calculate download progress
|
||||||
|
|
||||||
|
const expected_file_size = utils.getExpectedFileSize(info);
|
||||||
|
|
||||||
|
const files_to_check_for_progress = [];
|
||||||
|
|
||||||
|
// store info in download for future use
|
||||||
|
if (Array.isArray(info)) {
|
||||||
|
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
|
||||||
|
} else {
|
||||||
|
files_to_check_for_progress.push(utils.removeFileExtension(info['_filename']));
|
||||||
|
}
|
||||||
|
|
||||||
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
|
||||||
|
finished_step: true,
|
||||||
|
options: options,
|
||||||
|
files_to_check_for_progress: files_to_check_for_progress,
|
||||||
|
expected_file_size: expected_file_size
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadQueuedFile(url, type, options) {
|
async function downloadQueuedFile(download_uid) {
|
||||||
|
logger.verbose(`Downloading ${download_uid}`);
|
||||||
}
|
return new Promise(async resolve => {
|
||||||
|
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||||
async function downloadFileByURL_exec(url, type, options) {
|
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||||
return new Promise(resolve => {
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false});
|
||||||
const download = db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
|
||||||
const url = download['url'];
|
const url = download['url'];
|
||||||
const type = download['type'];
|
const type = download['type'];
|
||||||
@@ -84,20 +155,22 @@ async function downloadFileByURL_exec(url, type, options) {
|
|||||||
const args = download['args'];
|
const args = download['args'];
|
||||||
const category = download['category'];
|
const category = download['category'];
|
||||||
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
|
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
|
||||||
if (options.user) {
|
if (options.customFileFolderPath) {
|
||||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
fileFolderPath = options.customFileFolderPath;
|
||||||
const user_path = path.join(usersFileFolder, options.user, type);
|
|
||||||
fs.ensureDirSync(user_path);
|
|
||||||
fileFolderPath = user_path + path.sep;
|
|
||||||
options.customFileFolderPath = fileFolderPath;
|
|
||||||
}
|
}
|
||||||
|
fs.ensureDirSync(fileFolderPath);
|
||||||
|
|
||||||
|
const start_time = Date.now();
|
||||||
|
|
||||||
|
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
|
||||||
|
|
||||||
// download file
|
// download file
|
||||||
youtubedl.exec(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
youtubedl.exec(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
||||||
const file_objs = [];
|
const file_objs = [];
|
||||||
let new_date = Date.now();
|
let end_time = Date.now();
|
||||||
let difference = (new_date - date)/1000;
|
let difference = (end_time - start_time)/1000;
|
||||||
logger.debug(`${is_audio ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
||||||
|
clearInterval(download_checker);
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error(err.stderr);
|
logger.error(err.stderr);
|
||||||
|
|
||||||
@@ -107,7 +180,6 @@ async function downloadFileByURL_exec(url, type, options) {
|
|||||||
if (output.length === 0 || output[0].length === 0) {
|
if (output.length === 0 || output[0].length === 0) {
|
||||||
// ERROR!
|
// ERROR!
|
||||||
logger.warn(`No output received for video download, check if it exists in your archive.`)
|
logger.warn(`No output received for video download, check if it exists in your archive.`)
|
||||||
|
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -127,11 +199,12 @@ async function downloadFileByURL_exec(url, type, options) {
|
|||||||
// get filepath with no extension
|
// get filepath with no extension
|
||||||
const filepath_no_extension = utils.removeFileExtension(output_json['_filename']);
|
const filepath_no_extension = utils.removeFileExtension(output_json['_filename']);
|
||||||
|
|
||||||
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
var full_file_path = filepath_no_extension + ext;
|
var full_file_path = filepath_no_extension + ext;
|
||||||
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
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
|
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||||
&& config.getConfigItem('ytdl_use_twitch_api') && config.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||||
let vodId = url.split('twitch.tv/videos/')[1];
|
let vodId = url.split('twitch.tv/videos/')[1];
|
||||||
vodId = vodId.split('?')[0];
|
vodId = vodId.split('?')[0];
|
||||||
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, options.user);
|
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, options.user);
|
||||||
@@ -143,6 +216,7 @@ async function downloadFileByURL_exec(url, type, options) {
|
|||||||
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
||||||
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
|
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +230,7 @@ async function downloadFileByURL_exec(url, type, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.cropFileSettings) {
|
if (options.cropFileSettings) {
|
||||||
await cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
// registers file in DB
|
// registers file in DB
|
||||||
@@ -184,10 +258,9 @@ async function downloadFileByURL_exec(url, type, options) {
|
|||||||
logger.error('Downloaded file failed to result in metadata object.');
|
logger.error('Downloaded file failed to result in metadata object.');
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve({
|
const file_uids = file_objs.map(file_obj => file_obj.uid);
|
||||||
file_uids: file_objs.map(file_obj => file_obj.uid),
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {finished_step: true, finished: true, percent_complete: 100, file_uids: file_uids, container: container});
|
||||||
container: container
|
resolve();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -196,17 +269,20 @@ async function downloadFileByURL_exec(url, type, options) {
|
|||||||
// helper functions
|
// helper functions
|
||||||
|
|
||||||
async function generateArgs(url, type, options) {
|
async function generateArgs(url, type, options) {
|
||||||
|
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||||
|
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||||
|
|
||||||
const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||||
const globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
const globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
||||||
const useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
const useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||||
const is_audio = type === 'audio';
|
const is_audio = type === 'audio';
|
||||||
|
|
||||||
const fileFolderPath = is_audio ? audioFolderPath : videoFolderPath;
|
let fileFolderPath = is_audio ? audioFolderPath : videoFolderPath;
|
||||||
|
|
||||||
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
|
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
|
||||||
|
|
||||||
const customArgs = options.customArgs;
|
const customArgs = options.customArgs;
|
||||||
const customOutput = options.customOutput;
|
let customOutput = options.customOutput;
|
||||||
const customQualityConfiguration = options.customQualityConfiguration;
|
const customQualityConfiguration = options.customQualityConfiguration;
|
||||||
|
|
||||||
// video-specific args
|
// video-specific args
|
||||||
@@ -246,7 +322,7 @@ async function generateArgs(url, type, options) {
|
|||||||
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json'];
|
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (qualityPath && options.downloading_method === 'exec') downloadConfig.push(...qualityPath);
|
if (qualityPath) downloadConfig.push(...qualityPath);
|
||||||
|
|
||||||
if (is_audio && !options.skip_audio_args) {
|
if (is_audio && !options.skip_audio_args) {
|
||||||
downloadConfig.push('-x');
|
downloadConfig.push('-x');
|
||||||
@@ -265,6 +341,8 @@ async function generateArgs(url, type, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useDefaultDownloadingAgent = config_api.getConfigItem('ytdl_use_default_downloading_agent');
|
||||||
|
const customDownloadingAgent = config_api.getConfigItem('ytdl_custom_downloading_agent');
|
||||||
if (!useDefaultDownloadingAgent && customDownloadingAgent) {
|
if (!useDefaultDownloadingAgent && customDownloadingAgent) {
|
||||||
downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent);
|
downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent);
|
||||||
}
|
}
|
||||||
@@ -284,7 +362,7 @@ async function generateArgs(url, type, options) {
|
|||||||
await fs.ensureFile(merged_path);
|
await fs.ensureFile(merged_path);
|
||||||
// merges blacklist and regular archive
|
// merges blacklist and regular archive
|
||||||
let inputPathList = [archive_path, blacklist_path];
|
let inputPathList = [archive_path, blacklist_path];
|
||||||
let status = await mergeFiles(inputPathList, merged_path);
|
await mergeFiles(inputPathList, merged_path);
|
||||||
|
|
||||||
options.merged_string = await fs.readFile(merged_path, "utf8");
|
options.merged_string = await fs.readFile(merged_path, "utf8");
|
||||||
|
|
||||||
@@ -324,7 +402,7 @@ async function generateArgs(url, type, options) {
|
|||||||
return downloadConfig;
|
return downloadConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideoInfoByURL(url, args = [], download = null) {
|
async function getVideoInfoByURL(url, args = [], download_uid = null) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
// remove bad args
|
// remove bad args
|
||||||
const new_args = [...args];
|
const new_args = [...args];
|
||||||
@@ -334,21 +412,85 @@ async function getVideoInfoByURL(url, args = [], download = null) {
|
|||||||
new_args.splice(archiveArgIndex, 2);
|
new_args.splice(archiveArgIndex, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// actually get info
|
new_args.push('--dump-json');
|
||||||
youtubedl.getInfo(url, new_args, (err, output) => {
|
|
||||||
|
youtubedl.exec(url, new_args, {maxBuffer: Infinity}, async (err, output) => {
|
||||||
if (output) {
|
if (output) {
|
||||||
resolve(output);
|
let outputs = [];
|
||||||
|
try {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs.push(output_json);
|
||||||
|
}
|
||||||
|
resolve(outputs.length === 1 ? outputs[0] : outputs);
|
||||||
|
} catch(e) {
|
||||||
|
logger.error(`Error while retrieving info on video with URL ${url} with the following message: output JSON could not be parsed. Output JSON: ${output}`);
|
||||||
|
if (download_uid) {
|
||||||
|
const error = 'Failed to get info, see server logs for specific error.';
|
||||||
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {error: error, paused: true});
|
||||||
|
}
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.error(`Error while retrieving info on video with URL ${url} with the following message: ${err}`);
|
logger.error(`Error while retrieving info on video with URL ${url} with the following message: ${err}`);
|
||||||
if (err.stderr) {
|
if (err.stderr) {
|
||||||
logger.error(`${err.stderr}`)
|
logger.error(`${err.stderr}`)
|
||||||
}
|
}
|
||||||
if (download) {
|
if (download_uid) {
|
||||||
download['error'] = `Failed pre-check for video info: ${err}`;
|
const error = 'Failed to get info, see server logs for specific error.';
|
||||||
updateDownloads();
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {error: error, paused: true});
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterArgs(args, isAudio) {
|
||||||
|
const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs'];
|
||||||
|
const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail'];
|
||||||
|
const args_to_remove = isAudio ? video_only_args : audio_only_args;
|
||||||
|
return args.filter(x => !args_to_remove.includes(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkDownloadPercent(download_uid) {
|
||||||
|
/*
|
||||||
|
This is more of an art than a science, we're just selecting files that start with the file name,
|
||||||
|
thus capturing the parts being downloaded in files named like so: '<video title>.<format>.<ext>.part'.
|
||||||
|
|
||||||
|
Any file that starts with <video title> will be counted as part of the "bytes downloaded", which will
|
||||||
|
be divided by the "total expected bytes."
|
||||||
|
*/
|
||||||
|
|
||||||
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
const files_to_check_for_progress = download['files_to_check_for_progress'];
|
||||||
|
const resulting_file_size = download['expected_file_size'];
|
||||||
|
|
||||||
|
if (!resulting_file_size) return;
|
||||||
|
|
||||||
|
let sum_size = 0;
|
||||||
|
glob(`{${files_to_check_for_progress.join(',')}, }*`, async (err, files) => {
|
||||||
|
files.forEach(async file => {
|
||||||
|
try {
|
||||||
|
const file_stats = fs.statSync(file);
|
||||||
|
if (file_stats && file_stats.size) {
|
||||||
|
sum_size += file_stats.size;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const percent_complete = (sum_size/resulting_file_size * 100).toFixed(2);
|
||||||
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {percent_complete: percent_complete});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ const subscriptions_api = require('../subscriptions');
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
|
|
||||||
db_api.initialize(db, users_db, logger);
|
db_api.initialize(db, users_db);
|
||||||
|
|
||||||
|
|
||||||
describe('Database', async function() {
|
describe('Database', async function() {
|
||||||
@@ -287,39 +287,42 @@ describe('Multi User', async function() {
|
|||||||
// });
|
// });
|
||||||
// });
|
// });
|
||||||
|
|
||||||
describe('Downloader', function() {
|
});
|
||||||
const url = '';
|
|
||||||
const options = {
|
|
||||||
ui_uid: uuid(),
|
|
||||||
user: 'admin'
|
|
||||||
}
|
|
||||||
|
|
||||||
const download = {
|
describe('Downloader', function() {
|
||||||
url: url,
|
const downloader_api = require('../downloader');
|
||||||
options: options,
|
downloader_api.initialize(db_api);
|
||||||
type: 'video'
|
const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
|
||||||
}
|
const options = {
|
||||||
|
ui_uid: uuid(),
|
||||||
|
user: 'admin'
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await db_api.connectToDB();
|
await db_api.connectToDB();
|
||||||
await db_api.removeAllRecords('download_queue');
|
await db_api.removeAllRecords('download_queue');
|
||||||
await db_api.insertRecordIntoTable('download_queue', download)
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('Get file info', async function() {
|
it('Get file info', async function() {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Download file', async function() {
|
it('Download file', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
||||||
|
console.log(returned_download);
|
||||||
|
await utils.wait(20000);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Queue file', async function() {
|
it('Queue file', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
||||||
|
console.log(returned_download);
|
||||||
|
await utils.wait(20000);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
it('Pause file', async function() {
|
||||||
|
|
||||||
it('Pause file', async function() {
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -184,10 +184,6 @@ function getExpectedFileSize(input_info_jsons) {
|
|||||||
|
|
||||||
let expected_filesize = 0;
|
let expected_filesize = 0;
|
||||||
info_jsons.forEach(info_json => {
|
info_jsons.forEach(info_json => {
|
||||||
if (info_json['filesize']) {
|
|
||||||
expected_filesize += info_json['filesize'];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const formats = info_json['format_id'].split('+');
|
const formats = info_json['format_id'].split('+');
|
||||||
let individual_expected_filesize = 0;
|
let individual_expected_filesize = 0;
|
||||||
formats.forEach(format_id => {
|
formats.forEach(format_id => {
|
||||||
|
|||||||
@@ -182,7 +182,7 @@
|
|||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile">
|
<div class="centered big" id="bar_div" *ngIf="current_download; else nofile">
|
||||||
<div class="margined">
|
<div class="margined">
|
||||||
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
|
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
|
||||||
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
||||||
|
|||||||
@@ -274,9 +274,9 @@ export class MainComponent implements OnInit {
|
|||||||
const customOutput = localStorage.getItem('customOutput');
|
const customOutput = localStorage.getItem('customOutput');
|
||||||
const youtubeUsername = localStorage.getItem('youtubeUsername');
|
const youtubeUsername = localStorage.getItem('youtubeUsername');
|
||||||
|
|
||||||
if (customArgs && customArgs !== 'null') { this.customArgs = customArgs };
|
if (customArgs && customArgs !== 'null') { this.customArgs = customArgs }
|
||||||
if (customOutput && customOutput !== 'null') { this.customOutput = customOutput };
|
if (customOutput && customOutput !== 'null') { this.customOutput = customOutput }
|
||||||
if (youtubeUsername && youtubeUsername !== 'null') { this.youtubeUsername = youtubeUsername };
|
if (youtubeUsername && youtubeUsername !== 'null') { this.youtubeUsername = youtubeUsername }
|
||||||
}
|
}
|
||||||
|
|
||||||
// get downloads routine
|
// get downloads routine
|
||||||
@@ -343,7 +343,7 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public goToFile(container, isAudio, uid) {
|
public goToFile(container, isAudio, uid) {
|
||||||
this.downloadHelper(container, isAudio ? 'audio' : 'video', false, false, null, true);
|
this.downloadHelper(container, isAudio ? 'audio' : 'video', false, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public goToPlaylist(playlistID, type) {
|
public goToPlaylist(playlistID, type) {
|
||||||
@@ -375,7 +375,7 @@ export class MainComponent implements OnInit {
|
|||||||
|
|
||||||
// download helpers
|
// download helpers
|
||||||
|
|
||||||
downloadHelper(container, type, is_playlist = false, force_view = false, new_download = null, navigate_mode = false) {
|
downloadHelper(container, type, is_playlist = false, force_view = false, navigate_mode = false) {
|
||||||
this.downloadingfile = false;
|
this.downloadingfile = false;
|
||||||
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
||||||
// do nothing
|
// do nothing
|
||||||
@@ -399,8 +399,8 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove download from current downloads
|
// // remove download from current downloads
|
||||||
this.removeDownloadFromCurrentDownloads(new_download);
|
// this.removeDownloadFromCurrentDownloads(new_download);
|
||||||
}
|
}
|
||||||
|
|
||||||
// download click handler
|
// download click handler
|
||||||
@@ -432,21 +432,11 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const type = this.audioOnly ? 'audio' : 'video';
|
const type = this.audioOnly ? 'audio' : 'video';
|
||||||
// create download object
|
// this.downloads.push(new_download);
|
||||||
const new_download: Download = {
|
// if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
|
||||||
uid: uuid(),
|
|
||||||
type: type,
|
|
||||||
percent_complete: 0,
|
|
||||||
url: this.url,
|
|
||||||
downloading: true,
|
|
||||||
is_playlist: this.url.includes('playlist'),
|
|
||||||
error: false
|
|
||||||
};
|
|
||||||
this.downloads.push(new_download);
|
|
||||||
if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
|
|
||||||
this.downloadingfile = true;
|
this.downloadingfile = true;
|
||||||
|
|
||||||
let customQualityConfiguration = type === 'audio' ? this.getSelectedAudioFormat() : this.getSelectedVideoFormat();
|
const customQualityConfiguration = type === 'audio' ? this.getSelectedAudioFormat() : this.getSelectedVideoFormat();
|
||||||
|
|
||||||
let cropFileSettings = null;
|
let cropFileSettings = null;
|
||||||
|
|
||||||
@@ -458,26 +448,19 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.postsService.downloadFile(this.url, type, (this.selectedQuality === '' ? null : this.selectedQuality),
|
this.postsService.downloadFile(this.url, type, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||||
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid, cropFileSettings).subscribe(res => {
|
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, cropFileSettings).subscribe(res => {
|
||||||
// update download object
|
console.log(res);
|
||||||
new_download.downloading = false;
|
this.current_download = res['download'];
|
||||||
new_download.percent_complete = 100;
|
this.downloadingfile = true;
|
||||||
|
|
||||||
const container = res['container'];
|
|
||||||
const is_playlist = res['file_uids'].length > 1;
|
|
||||||
|
|
||||||
this.current_download = null;
|
|
||||||
|
|
||||||
this.downloadHelper(container, type, is_playlist, false, new_download);
|
|
||||||
}, error => { // can't access server
|
}, error => { // can't access server
|
||||||
this.downloadingfile = false;
|
this.downloadingfile = false;
|
||||||
this.current_download = null;
|
this.current_download = null;
|
||||||
new_download['downloading'] = false;
|
// new_download['downloading'] = false;
|
||||||
// removes download from list of downloads
|
// // removes download from list of downloads
|
||||||
const downloads_index = this.downloads.indexOf(new_download);
|
// const downloads_index = this.downloads.indexOf(new_download);
|
||||||
if (downloads_index !== -1) {
|
// if (downloads_index !== -1) {
|
||||||
this.downloads.splice(downloads_index)
|
// this.downloads.splice(downloads_index)
|
||||||
}
|
// }
|
||||||
this.openSnackBar('Download failed!', 'OK.');
|
this.openSnackBar('Download failed!', 'OK.');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -934,12 +917,16 @@ export class MainComponent implements OnInit {
|
|||||||
if (!this.current_download) {
|
if (!this.current_download) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ui_uid = this.current_download['ui_uid'] ? this.current_download['ui_uid'] : this.current_download['uid'];
|
this.postsService.getCurrentDownload(this.current_download['uid']).subscribe(res => {
|
||||||
this.postsService.getCurrentDownload(this.postsService.session_id, ui_uid).subscribe(res => {
|
|
||||||
if (res['download']) {
|
if (res['download']) {
|
||||||
if (ui_uid === res['download']['ui_uid']) {
|
this.current_download = res['download'];
|
||||||
this.current_download = res['download'];
|
this.percentDownloaded = this.current_download.percent_complete;
|
||||||
this.percentDownloaded = this.current_download.percent_complete;
|
|
||||||
|
if (this.current_download['finished']) {
|
||||||
|
const container = this.current_download['container'];
|
||||||
|
const is_playlist = this.current_download['file_uids'].length > 1;
|
||||||
|
this.downloadHelper(container, this.current_download['type'], is_playlist, false);
|
||||||
|
this.current_download = null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// console.log('failed to get new download');
|
// console.log('failed to get new download');
|
||||||
|
|||||||
@@ -345,22 +345,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return JSON.stringify(this.playlist) !== this.original_playlist;
|
return JSON.stringify(this.playlist) !== this.original_playlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlaylist() {
|
|
||||||
const fileNames = this.getFileNames();
|
|
||||||
this.playlist_updating = true;
|
|
||||||
this.postsService.updatePlaylistFiles(this.playlist_id, fileNames, this.type).subscribe(res => {
|
|
||||||
this.playlist_updating = false;
|
|
||||||
if (res['success']) {
|
|
||||||
const fileNamesEncoded = fileNames.join('|nvr|');
|
|
||||||
this.router.navigate(['/player', {fileNames: fileNamesEncoded, type: this.type, id: this.playlist_id}]);
|
|
||||||
this.openSnackBar('Successfully updated playlist.', '');
|
|
||||||
this.original_playlist = JSON.stringify(this.playlist);
|
|
||||||
} else {
|
|
||||||
this.openSnackBar('ERROR: Failed to update playlist.', '');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
openShareDialog() {
|
openShareDialog() {
|
||||||
const dialogRef = this.dialog.open(ShareMediaDialogComponent, {
|
const dialogRef = this.dialog.open(ShareMediaDialogComponent, {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export class PostsService implements CanActivate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line: max-line-length
|
// tslint:disable-next-line: max-line-length
|
||||||
downloadFile(url: string, type: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null, cropFileSettings = null) {
|
downloadFile(url: string, type: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, cropFileSettings = null) {
|
||||||
return this.http.post(this.path + 'downloadFile', {url: url,
|
return this.http.post(this.path + 'downloadFile', {url: url,
|
||||||
selectedHeight: selectedQuality,
|
selectedHeight: selectedQuality,
|
||||||
customQualityConfiguration: customQualityConfiguration,
|
customQualityConfiguration: customQualityConfiguration,
|
||||||
@@ -182,7 +182,6 @@ export class PostsService implements CanActivate {
|
|||||||
customOutput: customOutput,
|
customOutput: customOutput,
|
||||||
youtubeUsername: youtubeUsername,
|
youtubeUsername: youtubeUsername,
|
||||||
youtubePassword: youtubePassword,
|
youtubePassword: youtubePassword,
|
||||||
ui_uid: ui_uid,
|
|
||||||
type: type,
|
type: type,
|
||||||
cropFileSettings: cropFileSettings}, this.httpOptions);
|
cropFileSettings: cropFileSettings}, this.httpOptions);
|
||||||
}
|
}
|
||||||
@@ -345,12 +344,6 @@ export class PostsService implements CanActivate {
|
|||||||
return this.http.post(this.path + 'updatePlaylist', {playlist: playlist}, this.httpOptions);
|
return this.http.post(this.path + 'updatePlaylist', {playlist: playlist}, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlaylistFiles(playlist_id, fileNames, type) {
|
|
||||||
return this.http.post(this.path + 'updatePlaylistFiles', {playlist_id: playlist_id,
|
|
||||||
fileNames: fileNames,
|
|
||||||
type: type}, this.httpOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
addFileToPlaylist(playlist_id, file_uid) {
|
addFileToPlaylist(playlist_id, file_uid) {
|
||||||
return this.http.post(this.path + 'addFileToPlaylist', {playlist_id: playlist_id,
|
return this.http.post(this.path + 'addFileToPlaylist', {playlist_id: playlist_id,
|
||||||
file_uid: file_uid},
|
file_uid: file_uid},
|
||||||
@@ -426,8 +419,8 @@ export class PostsService implements CanActivate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// current download
|
// current download
|
||||||
getCurrentDownload(session_id, download_id) {
|
getCurrentDownload(download_uid) {
|
||||||
return this.http.post(this.path + 'download', {download_id: download_id, session_id: session_id}, this.httpOptions);
|
return this.http.post(this.path + 'download', {download_uid: download_uid}, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear downloads. download_id is optional, if it exists only 1 download will be cleared
|
// clear downloads. download_id is optional, if it exists only 1 download will be cleared
|
||||||
|
|||||||
Reference in New Issue
Block a user