From 01b6e22f83861479620bf39200d85f454b78d049 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Mon, 2 Aug 2021 18:41:30 -0600 Subject: [PATCH 01/27] Began scaffolding work for download manager --- backend/downloader.js | 284 ++++++++++++++++++++++++++++++++++++++++++ backend/test/tests.js | 35 ++++++ 2 files changed, 319 insertions(+) create mode 100644 backend/downloader.js diff --git a/backend/downloader.js b/backend/downloader.js new file mode 100644 index 00000000..4e44e86b --- /dev/null +++ b/backend/downloader.js @@ -0,0 +1,284 @@ + + +async function collectInfo(download_uid) { + const download = db_api.getRecord('download_queue', {uid: download_uid}); + + const url = download['url']; + const type = download['type']; + const options = download['options']; + const args = download['args']; + + // get video info prior to download + const info = await getVideoInfoByURL(url, args); + + if (!info) { + // info failed, record error and pause download + } + + // 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); + + // 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']) { + options.customOutput = category['custom_output']; + options.noRelativePath = true; + args = await generateArgs(url, type, options); + info = await getVideoInfoByURL(url, args); + + // 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}); +} + +async function downloadFileByURL_exec(url, type, options) { + const download = db_api.getRecord('download_queue', {uid: download_uid}); + + const url = download['url']; + const type = download['type']; + const options = download['options']; + const args = download['args']; + const category = download['category']; + let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix + if (options.user) { + let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); + const user_path = path.join(usersFileFolder, options.user, type); + fs.ensureDirSync(user_path); + fileFolderPath = user_path + path.sep; + multiUserMode = { + user: options.user, + file_path: fileFolderPath + } + options.customFileFolderPath = fileFolderPath; + } + +} + +async function downloadFileByURL_exec_old(url, type, options, sessionID = null) { + return new Promise(async resolve => { + var date = Date.now(); + + // audio / video specific vars + var is_audio = type === 'audio'; + var ext = is_audio ? '.mp3' : '.mp4'; + var fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; + let category = null; + + // prepend with user if needed + let multiUserMode = null; + if (options.user) { + let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); + const user_path = path.join(usersFileFolder, options.user, type); + fs.ensureDirSync(user_path); + fileFolderPath = user_path + path.sep; + options.customFileFolderPath = fileFolderPath; + } + + options.downloading_method = 'exec'; + let downloadConfig = await generateArgs(url, type, options); + + // adds download to download helper + const download_uid = uuid(); + const session = sessionID ? sessionID : 'undeclared'; + let session_downloads = downloads.find(potential_session_downloads => potential_session_downloads['session_id'] === session); + if (!session_downloads) { + session_downloads = {session_id: session}; + downloads.push(session_downloads); + } + session_downloads[download_uid] = { + uid: download_uid, + ui_uid: options.ui_uid, + downloading: true, + complete: false, + url: url, + type: type, + percent_complete: 0, + is_playlist: url.includes('playlist'), + timestamp_start: Date.now(), + filesize: null + }; + const download = session_downloads[download_uid]; + updateDownloads(); + + let download_checker = null; + + // get video info prior to download + let info = await getVideoInfoByURL(url, downloadConfig, download); + if (!info && url.includes('youtu')) { + resolve(false); + return; + } else if (info) { + // check if it fits into a category. If so, then get info again using new downloadConfig + if (!Array.isArray(info) || 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']) { + options.customOutput = category['custom_output']; + options.noRelativePath = true; + downloadConfig = await generateArgs(url, type, options); + info = await getVideoInfoByURL(url, downloadConfig, download); + } + + // store info in download for future use + if (Array.isArray(info)) { + download['fileNames'] = []; + for (let info_obj of info) download['fileNames'].push(info_obj['_filename']); + } else { + download['_filename'] = info['_filename']; + } + download['filesize'] = utils.getExpectedFileSize(info); + download_checker = setInterval(() => checkDownloadPercent(download), 1000); + } + + // download file + youtubedl.exec(url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) { + if (download_checker) clearInterval(download_checker); // stops the download checker from running as the download finished (or errored) + + download['downloading'] = false; + download['timestamp_end'] = Date.now(); + var file_objs = []; + let new_date = Date.now(); + let difference = (new_date - date)/1000; + logger.debug(`${is_audio ? 'Audio' : 'Video'} download delay: ${difference} seconds.`); + if (err) { + logger.error(err.stderr); + + download['error'] = err.stderr; + updateDownloads(); + resolve(false); + return; + } else if (output) { + if (output.length === 0 || output[0].length === 0) { + download['error'] = 'No output. Check if video already exists in your archive.'; + logger.warn(`No output received for video download, check if it exists in your archive.`) + updateDownloads(); + + resolve(false); + return; + } + var file_names = []; + 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; + } + + // get filepath with no extension + const filepath_no_extension = utils.removeFileExtension(output_json['_filename']); + + var full_file_path = filepath_no_extension + ext; + var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length); + + if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1 + && config.getConfigItem('ytdl_use_twitch_api') && config.getConfigItem('ytdl_twitch_auto_download_chat')) { + let vodId = url.split('twitch.tv/videos/')[1]; + vodId = vodId.split('?')[0]; + twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, options.user); + } + + // renames file if necessary due to bug + if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) { + try { + fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']); + logger.info('Renamed ' + file_name + '.webm to ' + file_name); + } catch(e) { + } + } + + if (type === 'audio') { + let tags = { + title: output_json['title'], + artist: output_json['artist'] ? output_json['artist'] : output_json['uploader'] + } + let success = NodeID3.write(tags, utils.removeFileExtension(output_json['_filename']) + '.mp3'); + if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']); + } + + const file_path = options.noRelativePath ? path.basename(full_file_path) : full_file_path.substring(fileFolderPath.length, full_file_path.length); + const customPath = options.noRelativePath ? path.dirname(full_file_path).split(path.sep).pop() : null; + + if (options.cropFileSettings) { + await cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext); + } + + // registers file in DB + const file_obj = await db_api.registerFileDB2(full_file_path, type, options.user, category, null, options.cropFileSettings); + + // TODO: remove the following line + if (file_name) file_names.push(file_name); + + file_objs.push(file_obj); + } + + let is_playlist = file_names.length > 1; + + if (options.merged_string !== null && options.merged_string !== undefined) { + let current_merged_archive = fs.readFileSync(path.join(fileFolderPath, `merged_${type}.txt`), 'utf8'); + let diff = current_merged_archive.replace(options.merged_string, ''); + const archive_path = options.user ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`); + fs.appendFileSync(archive_path, diff); + } + + download['complete'] = true; + download['fileNames'] = is_playlist ? file_names : [full_file_path] + updateDownloads(); + + let container = null; + + if (file_objs.length > 1) { + // create playlist + const playlist_name = file_objs.map(file_obj => file_obj.title).join(', '); + const duration = file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0); + container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), type, options.user); + } else if (file_objs.length === 1) { + container = file_objs[0]; + } else { + logger.error('Downloaded file failed to result in metadata object.'); + } + + resolve({ + file_uids: file_objs.map(file_obj => file_obj.uid), + container: container + }); + } + }); + }); +} + +// helper functions + +async function getVideoInfoByURL(url, args = [], download = null) { + return new Promise(resolve => { + // remove bad args + const new_args = [...args]; + + const archiveArgIndex = new_args.indexOf('--download-archive'); + if (archiveArgIndex !== -1) { + new_args.splice(archiveArgIndex, 2); + } + + // actually get info + youtubedl.getInfo(url, new_args, (err, output) => { + if (output) { + resolve(output); + } else { + logger.error(`Error while retrieving info on video with URL ${url} with the following message: ${err}`); + if (err.stderr) { + logger.error(`${err.stderr}`) + } + if (download) { + download['error'] = `Failed pre-check for video info: ${err}`; + updateDownloads(); + } + resolve(null); + } + }); + }); +} \ No newline at end of file diff --git a/backend/test/tests.js b/backend/test/tests.js index 17ae27c5..9f19505a 100644 --- a/backend/test/tests.js +++ b/backend/test/tests.js @@ -287,4 +287,39 @@ describe('Multi User', async function() { // }); // }); + describe('Downloader', function() { + const url = ''; + const options = { + ui_uid: uuid(), + user: 'admin' + } + + const download = { + url: url, + options: options, + type: 'video' + } + + beforeEach(async function() { + await db_api.connectToDB(); + await db_api.removeAllRecords('download_queue'); + await db_api.insertRecordIntoTable('download_queue', download) + }); + + it('Get file info', async function() { + + }); + + it('Download file', async function() { + + }); + + it('Queue file', async function() { + + }); + + it('Pause file', async function() { + + }); + }); }); \ No newline at end of file From 2927a4564d189f28c5a8f63ab23010fe1e305c5b Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Thu, 5 Aug 2021 18:57:54 -0600 Subject: [PATCH 02/27] Additional scaffolding for download manager Added queue to npm backend dependencies --- backend/downloader.js | 294 +++++++++++++++++++++++--------------- backend/package-lock.json | 8 ++ backend/package.json | 1 + 3 files changed, 191 insertions(+), 112 deletions(-) diff --git a/backend/downloader.js b/backend/downloader.js index 4e44e86b..5054e060 100644 --- a/backend/downloader.js +++ b/backend/downloader.js @@ -1,4 +1,42 @@ +const fs = require('fs-extra'); +const { uuid } = require('uuidv4'); +const path = require('path'); +const queue = require('queue'); +const youtubedl = require('youtube-dl'); +const config_api = require('./config'); +const twitch_api = require('./twitch'); +const utils = require('./utils'); + +let db_api = null; +let logger = null; + +function setDB(input_db_api) { db_api = input_db_api } +function setLogger(input_logger) { logger = input_logger; } + +exports.initialize = (input_db_api, input_logger) => { + setDB(input_db_api); + setLogger(input_logger); +} + +exports.pauseDownload = () => { + +} + +async function checkDownloads() { + const downloads = await db_api.getRecords('download_queue'); + downloads.sort((download1, download2) => download1.timestamp_start - download2.timestamp_start); + downloads = downloads.filter(download => !download.paused); + for (let i = 0; i < downloads.length; i++) { + if (i === config_api.getConfigItem('ytdl_')) + } +} + +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) { const download = db_api.getRecord('download_queue', {uid: download_uid}); @@ -32,41 +70,20 @@ async function collectInfo(download_uid) { await db_api.updateRecord('download_queue', {uid: download_uid}, {remote_metadata: info}); } -async function downloadFileByURL_exec(url, type, options) { - const download = db_api.getRecord('download_queue', {uid: download_uid}); - - const url = download['url']; - const type = download['type']; - const options = download['options']; - const args = download['args']; - const category = download['category']; - let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix - if (options.user) { - let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); - const user_path = path.join(usersFileFolder, options.user, type); - fs.ensureDirSync(user_path); - fileFolderPath = user_path + path.sep; - multiUserMode = { - user: options.user, - file_path: fileFolderPath - } - options.customFileFolderPath = fileFolderPath; - } +async function downloadQueuedFile(url, type, options) { } -async function downloadFileByURL_exec_old(url, type, options, sessionID = null) { - return new Promise(async resolve => { - var date = Date.now(); +async function downloadFileByURL_exec(url, type, options) { + return new Promise(resolve => { + const download = db_api.getRecord('download_queue', {uid: download_uid}); - // audio / video specific vars - var is_audio = type === 'audio'; - var ext = is_audio ? '.mp3' : '.mp4'; - var fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; - let category = null; - - // prepend with user if needed - let multiUserMode = null; + const url = download['url']; + const type = download['type']; + const options = download['options']; + const args = download['args']; + const category = download['category']; + let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix if (options.user) { let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); const user_path = path.join(usersFileFolder, options.user, type); @@ -75,89 +92,26 @@ async function downloadFileByURL_exec_old(url, type, options, sessionID = null) options.customFileFolderPath = fileFolderPath; } - options.downloading_method = 'exec'; - let downloadConfig = await generateArgs(url, type, options); - - // adds download to download helper - const download_uid = uuid(); - const session = sessionID ? sessionID : 'undeclared'; - let session_downloads = downloads.find(potential_session_downloads => potential_session_downloads['session_id'] === session); - if (!session_downloads) { - session_downloads = {session_id: session}; - downloads.push(session_downloads); - } - session_downloads[download_uid] = { - uid: download_uid, - ui_uid: options.ui_uid, - downloading: true, - complete: false, - url: url, - type: type, - percent_complete: 0, - is_playlist: url.includes('playlist'), - timestamp_start: Date.now(), - filesize: null - }; - const download = session_downloads[download_uid]; - updateDownloads(); - - let download_checker = null; - - // get video info prior to download - let info = await getVideoInfoByURL(url, downloadConfig, download); - if (!info && url.includes('youtu')) { - resolve(false); - return; - } else if (info) { - // check if it fits into a category. If so, then get info again using new downloadConfig - if (!Array.isArray(info) || 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']) { - options.customOutput = category['custom_output']; - options.noRelativePath = true; - downloadConfig = await generateArgs(url, type, options); - info = await getVideoInfoByURL(url, downloadConfig, download); - } - - // store info in download for future use - if (Array.isArray(info)) { - download['fileNames'] = []; - for (let info_obj of info) download['fileNames'].push(info_obj['_filename']); - } else { - download['_filename'] = info['_filename']; - } - download['filesize'] = utils.getExpectedFileSize(info); - download_checker = setInterval(() => checkDownloadPercent(download), 1000); - } - // download file - youtubedl.exec(url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) { - if (download_checker) clearInterval(download_checker); // stops the download checker from running as the download finished (or errored) - - download['downloading'] = false; - download['timestamp_end'] = Date.now(); - var file_objs = []; + youtubedl.exec(url, args, {maxBuffer: Infinity}, async function(err, output) { + const file_objs = []; let new_date = Date.now(); let difference = (new_date - date)/1000; logger.debug(`${is_audio ? 'Audio' : 'Video'} download delay: ${difference} seconds.`); if (err) { logger.error(err.stderr); - download['error'] = err.stderr; - updateDownloads(); resolve(false); return; } else if (output) { if (output.length === 0 || output[0].length === 0) { - download['error'] = 'No output. Check if video already exists in your archive.'; + // ERROR! logger.warn(`No output received for video download, check if it exists in your archive.`) - updateDownloads(); resolve(false); return; } - var file_names = []; + for (let i = 0; i < output.length; i++) { let output_json = null; try { @@ -201,9 +155,6 @@ async function downloadFileByURL_exec_old(url, type, options, sessionID = null) if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']); } - const file_path = options.noRelativePath ? path.basename(full_file_path) : full_file_path.substring(fileFolderPath.length, full_file_path.length); - const customPath = options.noRelativePath ? path.dirname(full_file_path).split(path.sep).pop() : null; - if (options.cropFileSettings) { await cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext); } @@ -211,14 +162,9 @@ async function downloadFileByURL_exec_old(url, type, options, sessionID = null) // registers file in DB const file_obj = await db_api.registerFileDB2(full_file_path, type, options.user, category, null, options.cropFileSettings); - // TODO: remove the following line - if (file_name) file_names.push(file_name); - file_objs.push(file_obj); } - let is_playlist = file_names.length > 1; - if (options.merged_string !== null && options.merged_string !== undefined) { let current_merged_archive = fs.readFileSync(path.join(fileFolderPath, `merged_${type}.txt`), 'utf8'); let diff = current_merged_archive.replace(options.merged_string, ''); @@ -226,16 +172,11 @@ async function downloadFileByURL_exec_old(url, type, options, sessionID = null) fs.appendFileSync(archive_path, diff); } - download['complete'] = true; - download['fileNames'] = is_playlist ? file_names : [full_file_path] - updateDownloads(); - let container = null; if (file_objs.length > 1) { // create playlist const playlist_name = file_objs.map(file_obj => file_obj.title).join(', '); - const duration = file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0); container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), type, options.user); } else if (file_objs.length === 1) { container = file_objs[0]; @@ -254,6 +195,135 @@ async function downloadFileByURL_exec_old(url, type, options, sessionID = null) // helper functions +async function generateArgs(url, type, options) { + 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 useCookies = config_api.getConfigItem('ytdl_use_cookies'); + const is_audio = type === 'audio'; + + const fileFolderPath = is_audio ? audioFolderPath : videoFolderPath; + + if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath; + + const customArgs = options.customArgs; + const customOutput = options.customOutput; + const customQualityConfiguration = options.customQualityConfiguration; + + // video-specific args + const selectedHeight = options.selectedHeight; + + // audio-specific args + const maxBitrate = options.maxBitrate; + + const youtubeUsername = options.youtubeUsername; + const youtubePassword = options.youtubePassword; + + let downloadConfig = null; + let qualityPath = (is_audio && !options.skip_audio_args) ? ['-f', 'bestaudio'] : ['-f', 'bestvideo+bestaudio', '--merge-output-format', 'mp4']; + const is_youtube = url.includes('youtu'); + if (!is_audio && !is_youtube) { + // tiktok videos fail when using the default format + qualityPath = null; + } else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) { + qualityPath = ['-f', 'bestvideo+bestaudio'] + } + + if (customArgs) { + downloadConfig = customArgs.split(',,'); + } else { + if (customQualityConfiguration) { + qualityPath = ['-f', customQualityConfiguration]; + } else if (selectedHeight && selectedHeight !== '' && !is_audio) { + qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`]; + } else if (is_audio) { + qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0'] + } + + if (customOutput) { + customOutput = options.noRelativePath ? customOutput : path.join(fileFolderPath, customOutput); + downloadConfig = ['-o', `${customOutput}.%(ext)s`, '--write-info-json', '--print-json']; + } else { + 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 (is_audio && !options.skip_audio_args) { + downloadConfig.push('-x'); + downloadConfig.push('--audio-format', 'mp3'); + } + + if (youtubeUsername && youtubePassword) { + downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword); + } + + if (useCookies) { + if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) { + downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt')); + } else { + logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.'); + } + } + + if (!useDefaultDownloadingAgent && customDownloadingAgent) { + downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent); + } + + let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); + if (useYoutubeDLArchive) { + const archive_folder = options.user ? path.join(fileFolderPath, 'archives') : archivePath; + const archive_path = path.join(archive_folder, `archive_${type}.txt`); + + await fs.ensureDir(archive_folder); + await fs.ensureFile(archive_path); + + let blacklist_path = options.user ? path.join(fileFolderPath, 'archives', `blacklist_${type}.txt`) : path.join(archivePath, `blacklist_${type}.txt`); + await fs.ensureFile(blacklist_path); + + let merged_path = path.join(fileFolderPath, `merged_${type}.txt`); + await fs.ensureFile(merged_path); + // merges blacklist and regular archive + let inputPathList = [archive_path, blacklist_path]; + let status = await mergeFiles(inputPathList, merged_path); + + options.merged_string = await fs.readFile(merged_path, "utf8"); + + downloadConfig.push('--download-archive', merged_path); + } + + if (config_api.getConfigItem('ytdl_include_thumbnail')) { + downloadConfig.push('--write-thumbnail'); + } + + if (globalArgs && globalArgs !== '') { + // adds global args + if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) { + // if global args has an output, replce the original output with that of global args + const original_output_index = downloadConfig.indexOf('-o'); + downloadConfig.splice(original_output_index, 2); + } + downloadConfig = downloadConfig.concat(globalArgs.split(',,')); + } + + const rate_limit = config_api.getConfigItem('ytdl_download_rate_limit'); + if (rate_limit && downloadConfig.indexOf('-r') === -1 && downloadConfig.indexOf('--limit-rate') === -1) { + downloadConfig.push('-r', rate_limit); + } + + const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader'); + if (default_downloader === 'yt-dlp') { + downloadConfig.push('--no-clean-infojson'); + } + + } + + // filter out incompatible args + downloadConfig = filterArgs(downloadConfig, is_audio); + + logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`); + return downloadConfig; +} + async function getVideoInfoByURL(url, args = [], download = null) { return new Promise(resolve => { // remove bad args @@ -281,4 +351,4 @@ async function getVideoInfoByURL(url, args = [], download = null) { } }); }); -} \ No newline at end of file +} diff --git a/backend/package-lock.json b/backend/package-lock.json index 57ee7559..7949f7d9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2719,6 +2719,14 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "requires": { + "inherits": "~2.0.3" + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index b2cd7c17..b4288827 100644 --- a/backend/package.json +++ b/backend/package.json @@ -59,6 +59,7 @@ "passport-local": "^1.0.0", "progress": "^2.0.3", "ps-node": "^0.1.6", + "queue": "^6.0.2", "read-last-lines": "^1.7.2", "shortid": "^2.2.15", "unzipper": "^0.10.10", From 5a90be7703fe2a4b500e66139d4d6d71daa611c8 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 8 Aug 2021 14:54:24 -0600 Subject: [PATCH 03/27] Logger is now separated into its own module Added eslint and fixed many logic errors based on its recommendations --- .eslintrc.json | 20 + backend/.eslintrc.json | 18 + backend/app.js | 160 ++---- backend/authentication/auth.js | 19 +- backend/categories.js | 41 +- backend/config.js | 10 +- backend/consts.js | 13 +- backend/db.js | 20 +- backend/logger.js | 23 + backend/subscriptions.js | 74 +-- backend/utils.js | 32 +- package-lock.json | 813 +++++++++++++++++++++++++++++ package.json | 3 + src/app/player/player.component.ts | 1 - 14 files changed, 1004 insertions(+), 243 deletions(-) create mode 100644 .eslintrc.json create mode 100644 backend/.eslintrc.json create mode 100644 backend/logger.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..c5624c4e --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + } +} diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json new file mode 100644 index 00000000..172a2e45 --- /dev/null +++ b/backend/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "env": { + "node": true, + "es2021": true + }, + "extends": [ + "eslint:recommended" + ], + "parser": "esprima", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": [], + "rules": { + }, + "root": true +} diff --git a/backend/app.js b/backend/app.js index a9025e9a..3154f051 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,35 +1,35 @@ const { uuid } = require('uuidv4'); -var fs = require('fs-extra'); -var { promisify } = require('util'); -var auth_api = require('./authentication/auth'); -var winston = require('winston'); -var path = require('path'); -var ffmpeg = require('fluent-ffmpeg'); -var compression = require('compression'); -var glob = require("glob") -var multer = require('multer'); -var express = require("express"); -var bodyParser = require("body-parser"); -var archiver = require('archiver'); -var unzipper = require('unzipper'); -var db_api = require('./db'); -var utils = require('./utils') -var mergeFiles = require('merge-files'); +const fs = require('fs-extra'); +const { promisify } = require('util'); +const auth_api = require('./authentication/auth'); +const winston = require('winston'); +const path = require('path'); +const compression = require('compression'); +const glob = require("glob") +const multer = require('multer'); +const express = require("express"); +const bodyParser = require("body-parser"); +const archiver = require('archiver'); +const unzipper = require('unzipper'); +const db_api = require('./db'); +const utils = require('./utils') +const mergeFiles = require('merge-files'); const low = require('lowdb') -var ProgressBar = require('progress'); +const ProgressBar = require('progress'); const NodeID3 = require('node-id3') const fetch = require('node-fetch'); -var URL = require('url').URL; +const URL = require('url').URL; const url_api = require('url'); const CONSTS = require('./consts') const read_last_lines = require('read-last-lines'); -var ps = require('ps-node'); +const ps = require('ps-node'); // 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"}) -var youtubedl = require('youtube-dl'); +const youtubedl = require('youtube-dl'); +const logger = require('./logger'); var config_api = require('./config.js'); var subscriptions_api = require('./subscriptions') var categories_api = require('./categories'); @@ -61,30 +61,11 @@ const admin_token = '4241b401-7236-493e-92b5-b72696b9d853'; // logging setup -// console format -const defaultFormat = winston.format.printf(({ level, message, label, timestamp }) => { - return `${timestamp} ${level.toUpperCase()}: ${message}`; -}); -const logger = winston.createLogger({ - level: 'info', - format: winston.format.combine(winston.format.timestamp(), defaultFormat), - defaultMeta: {}, - transports: [ - // - // - Write to all logs with level `info` and below to `combined.log` - // - Write all logs error (and below) to `error.log`. - // - new winston.transports.File({ filename: 'appdata/logs/error.log', level: 'error' }), - new winston.transports.File({ filename: 'appdata/logs/combined.log' }), - new winston.transports.Console({level: !debugMode ? 'info' : 'debug', name: 'console'}) - ] -}); - -config_api.initialize(logger); -db_api.initialize(db, users_db, logger); -auth_api.initialize(db_api, logger); -subscriptions_api.initialize(db_api, logger); -categories_api.initialize(db, users_db, logger, db_api); +config_api.initialize(); +db_api.initialize(db, users_db); +auth_api.initialize(db_api); +subscriptions_api.initialize(db_api); +categories_api.initialize(db_api); // Set some defaults db.defaults( @@ -122,13 +103,9 @@ users_db.defaults( ).write(); // config values -var frontendUrl = null; -var backendUrl = null; var backendPort = null; -var basePath = null; var audioFolderPath = null; var videoFolderPath = null; -var downloadOnlyMode = null; var useDefaultDownloadingAgent = null; var customDownloadingAgent = null; var allowSubscriptions = null; @@ -138,8 +115,6 @@ var archivePath = path.join(__dirname, 'appdata', 'archives'); var url_domain = null; var updaterStatus = null; -var timestamp_server_start = Date.now(); - const concurrentStreams = {}; if (debugMode) logger.info('YTDL-Material in debug mode!'); @@ -368,6 +343,7 @@ async function updateServer(tag) { } restartServer(true); }, err => { + logger.error(err); updaterStatus = { updating: false, error: true, @@ -398,12 +374,10 @@ async function downloadReleaseFiles(tag) { fs.createReadStream(path.join(__dirname, `youtubedl-material-release-${tag}.zip`)).pipe(unzipper.Parse()) .on('entry', function (entry) { var fileName = entry.path; - var type = entry.type; // 'Directory' or 'File' - var size = entry.size; var is_dir = fileName.substring(fileName.length-1, fileName.length) === '/' if (!is_dir && fileName.includes('youtubedl-material/public/')) { // get public folder files - var actualFileName = fileName.replace('youtubedl-material/public/', ''); + const actualFileName = fileName.replace('youtubedl-material/public/', ''); if (actualFileName.length !== 0 && actualFileName.substring(actualFileName.length-1, actualFileName.length) !== '/') { fs.ensureDirSync(path.join(__dirname, 'public', path.dirname(actualFileName))); entry.pipe(fs.createWriteStream(path.join(__dirname, 'public', actualFileName))); @@ -412,7 +386,7 @@ async function downloadReleaseFiles(tag) { } } else if (!is_dir && !replace_ignore_list.includes(fileName)) { // get package.json - var actualFileName = fileName.replace('youtubedl-material/', ''); + const actualFileName = fileName.replace('youtubedl-material/', ''); logger.verbose('Downloading file ' + actualFileName); entry.pipe(fs.createWriteStream(path.join(__dirname, actualFileName))); } else { @@ -750,17 +724,6 @@ function generateEnvVarConfigItem(key) { return {key: key, value: process['env'][key]}; } -function getVideoFormatID(name) -{ - var jsonPath = videoFolderPath+name+".info.json"; - if (fs.existsSync(jsonPath)) - { - var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); - var format = obj.format.substring(0,3); - return format; - } -} - /** * @param {'audio' | 'video'} type * @param {string[]} fileNames @@ -808,16 +771,11 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { let category = null; // prepend with user if needed - let multiUserMode = null; if (options.user) { let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); const user_path = path.join(usersFileFolder, options.user, type); fs.ensureDirSync(user_path); fileFolderPath = user_path + path.sep; - multiUserMode = { - user: options.user, - file_path: fileFolderPath - } options.customFileFolderPath = fileFolderPath; } @@ -947,11 +905,8 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']); } - const file_path = options.noRelativePath ? path.basename(full_file_path) : full_file_path.substring(fileFolderPath.length, full_file_path.length); - const customPath = options.noRelativePath ? path.dirname(full_file_path).split(path.sep).pop() : null; - 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 @@ -1022,6 +977,8 @@ async function generateArgs(url, type, options) { var youtubePassword = options.youtubePassword; let downloadConfig = null; + + // TODO: fix let qualityPath = (is_audio && !options.skip_audio_args) ? ['-f', 'bestaudio'] : ['-f', 'bestvideo+bestaudio', '--merge-output-format', 'mp4']; const is_youtube = url.includes('youtu'); if (!is_audio && !is_youtube) { @@ -1197,33 +1154,6 @@ async function getUrlInfos(urls) { }); } -// ffmpeg helper functions - -async function cropFile(file_path, start, end, ext) { - return new Promise(resolve => { - const temp_file_path = `${file_path}.cropped${ext}`; - let base_ffmpeg_call = ffmpeg(file_path); - if (start) { - base_ffmpeg_call = base_ffmpeg_call.seekOutput(start); - } - if (end) { - base_ffmpeg_call = base_ffmpeg_call.duration(end - start); - } - base_ffmpeg_call - .on('end', () => { - logger.verbose(`Cropping for '${file_path}' complete.`); - fs.unlinkSync(file_path); - fs.moveSync(temp_file_path, file_path); - resolve(true); - }) - .on('error', (err, test, test2) => { - logger.error(`Failed to crop ${file_path}.`); - logger.error(err); - resolve(false); - }).save(temp_file_path); - }); -} - // download management functions async function updateDownloads() { @@ -1951,14 +1881,12 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => { let subID = req.body.id; let subName = req.body.name; // if included, subID is optional - let user_uid = req.isAuthenticated() ? req.user.uid : null; - // get sub from db let subscription = null; if (subID) { - subscription = await subscriptions_api.getSubscription(subID, user_uid) + subscription = await subscriptions_api.getSubscription(subID) } else if (subName) { - subscription = await subscriptions_api.getSubscriptionByName(subName, user_uid) + subscription = await subscriptions_api.getSubscriptionByName(subName) } if (!subscription) { @@ -2124,28 +2052,6 @@ app.post('/api/getPlaylists', optionalJwt, async (req, res) => { }); }); -app.post('/api/updatePlaylistFiles', optionalJwt, async (req, res) => { - let playlistID = req.body.playlist_id; - let uids = req.body.uids; - - let success = false; - try { - if (req.isAuthenticated()) { - auth_api.updatePlaylistFiles(req.user.uid, playlistID, uids); - } else { - await db_api.updateRecord('playlists', {id: playlistID}, {uids: uids}) - } - - success = true; - } catch(e) { - logger.error(`Failed to find playlist with ID ${playlistID}`); - } - - res.send({ - success: success - }) -}); - app.post('/api/addFileToPlaylist', optionalJwt, async (req, res) => { let playlist_id = req.body.playlist_id; let file_uid = req.body.file_uid; diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index 7992aac3..caea4dae 100644 --- a/backend/authentication/auth.js +++ b/backend/authentication/auth.js @@ -1,7 +1,7 @@ -const path = require('path'); const config_api = require('../config'); const consts = require('../consts'); -const fs = require('fs-extra'); +const logger = require('../logger'); + const jwt = require('jsonwebtoken'); const { uuid } = require('uuidv4'); const bcrypt = require('bcryptjs'); @@ -12,15 +12,13 @@ var JwtStrategy = require('passport-jwt').Strategy, ExtractJwt = require('passport-jwt').ExtractJwt; // other required vars -let logger = null; let db_api = null; let SERVER_SECRET = null; let JWT_EXPIRATION = null; let opts = null; let saltRounds = null; -exports.initialize = function(db_api, input_logger) { - setLogger(input_logger) +exports.initialize = function(db_api) { setDB(db_api); /************************* @@ -53,10 +51,6 @@ exports.initialize = function(db_api, input_logger) { })); } -function setLogger(input_logger) { - logger = input_logger; -} - function setDB(input_db_api) { db_api = input_db_api; } @@ -291,17 +285,12 @@ exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false return file; } -exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames) { - users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}).assign({fileNames: new_filenames}); - return true; -} - exports.removePlaylist = async function(user_uid, playlistID) { await db_api.removeRecord('playlist', {playlistID: playlistID}); return true; } -exports.getUserPlaylists = async function(user_uid, user_files = null) { +exports.getUserPlaylists = async function(user_uid) { return await db_api.getRecords('playlists', {user_uid: user_uid}); } diff --git a/backend/categories.js b/backend/categories.js index fb33a886..269ae9c9 100644 --- a/backend/categories.js +++ b/backend/categories.js @@ -1,17 +1,12 @@ -const config_api = require('./config'); const utils = require('./utils'); +const logger = require('./logger'); -var logger = null; -var db = null; -var users_db = null; var db_api = null; -function setDB(input_db, input_users_db, input_db_api) { db = input_db; users_db = input_users_db; db_api = input_db_api } -function setLogger(input_logger) { logger = input_logger; } +function setDB(input_db_api) { db_api = input_db_api } -function initialize(input_db, input_users_db, input_logger, input_db_api) { - setDB(input_db, input_users_db, input_db_api); - setLogger(input_logger); +function initialize(input_db_api) { + setDB(input_db_api); } /* @@ -72,7 +67,7 @@ async function getCategoriesAsPlaylists(files = null) { const categories_as_playlists = []; const available_categories = await getCategories(); if (available_categories && files) { - for (category of available_categories) { + for (let category of available_categories) { const files_that_match = utils.addUIDsToCategory(category, files); if (files_that_match && files_that_match.length > 0) { category['thumbnailURL'] = files_that_match[0].thumbnailURL; @@ -125,21 +120,21 @@ function applyCategoryRules(file_json, rules, category_name) { return rules_apply; } -async function addTagToVideo(tag, video, user_uid) { - // TODO: Implement -} +// async function addTagToVideo(tag, video, user_uid) { +// // TODO: Implement +// } -async function removeTagFromVideo(tag, video, user_uid) { - // TODO: Implement -} +// async function removeTagFromVideo(tag, video, user_uid) { +// // TODO: Implement +// } -// adds tag to list of existing tags (used for tag suggestions) -async function addTagToExistingTags(tag) { - const existing_tags = db.get('tags').value(); - if (!existing_tags.includes(tag)) { - db.get('tags').push(tag).write(); - } -} +// // adds tag to list of existing tags (used for tag suggestions) +// async function addTagToExistingTags(tag) { +// const existing_tags = db.get('tags').value(); +// if (!existing_tags.includes(tag)) { +// db.get('tags').push(tag).write(); +// } +// } module.exports = { initialize: initialize, diff --git a/backend/config.js b/backend/config.js index c0151d70..c8a8aaa6 100644 --- a/backend/config.js +++ b/backend/config.js @@ -1,3 +1,5 @@ +const logger = require('./logger'); + const fs = require('fs'); let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS']; @@ -5,11 +7,7 @@ const debugMode = process.env.YTDL_MODE === 'debug'; let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json'; -var logger = null; -function setLogger(input_logger) { logger = input_logger; } - -function initialize(input_logger) { - setLogger(input_logger); +function initialize() { ensureConfigFileExists(); ensureConfigItemsExist(); } @@ -175,7 +173,7 @@ module.exports = { globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload } -DEFAULT_CONFIG = { +const DEFAULT_CONFIG = { "YoutubeDLMaterial": { "Host": { "url": "http://example.com", diff --git a/backend/consts.js b/backend/consts.js index b26eb917..28ee62c3 100644 --- a/backend/consts.js +++ b/backend/consts.js @@ -1,4 +1,4 @@ -let CONFIG_ITEMS = { +exports.CONFIG_ITEMS = { // Host 'ytdl_url': { 'key': 'ytdl_url', @@ -210,7 +210,7 @@ let CONFIG_ITEMS = { } }; -AVAILABLE_PERMISSIONS = [ +exports.AVAILABLE_PERMISSIONS = [ 'filemanager', 'settings', 'subscriptions', @@ -219,11 +219,6 @@ AVAILABLE_PERMISSIONS = [ 'downloads_manager' ]; -const DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details' +exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details' -module.exports = { - CONFIG_ITEMS: CONFIG_ITEMS, - AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS, - CURRENT_VERSION: 'v4.2', - DETAILS_BIN_PATH: DETAILS_BIN_PATH -} +exports.CURRENT_VERSION = 'v4.2'; diff --git a/backend/db.js b/backend/db.js index 802694d6..74a92c52 100644 --- a/backend/db.js +++ b/backend/db.js @@ -1,19 +1,18 @@ var fs = require('fs-extra') var path = require('path') -var utils = require('./utils') -const { uuid } = require('uuidv4'); -const config_api = require('./config'); const { MongoClient } = require("mongodb"); +const { uuid } = require('uuidv4'); + +const config_api = require('./config'); +var utils = require('./utils') +const logger = require('./logger'); const low = require('lowdb') const FileSync = require('lowdb/adapters/FileSync'); const local_adapter = new FileSync('./appdata/local_db.json'); const local_db = low(local_adapter); -var logger = null; -var db = null; -var users_db = null; -var database = null; +let database = null; const tables = { files: { @@ -62,13 +61,8 @@ function setDB(input_db, input_users_db) { exports.users_db = input_users_db } -function setLogger(input_logger) { - logger = input_logger; -} - -exports.initialize = (input_db, input_users_db, input_logger) => { +exports.initialize = (input_db, input_users_db) => { setDB(input_db, input_users_db); - setLogger(input_logger); // must be done here to prevent getConfigItem from being called before init using_local_db = config_api.getConfigItem('ytdl_use_local_db'); diff --git a/backend/logger.js b/backend/logger.js new file mode 100644 index 00000000..9bdd429b --- /dev/null +++ b/backend/logger.js @@ -0,0 +1,23 @@ +const winston = require('winston'); + +let debugMode = process.env.YTDL_MODE === 'debug'; + +const defaultFormat = winston.format.printf(({ level, message, label, timestamp }) => { + return `${timestamp} ${level.toUpperCase()}: ${message}`; +}); +const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine(winston.format.timestamp(), defaultFormat), + defaultMeta: {}, + transports: [ + // + // - Write to all logs with level `info` and below to `combined.log` + // - Write all logs error (and below) to `error.log`. + // + new winston.transports.File({ filename: 'appdata/logs/error.log', level: 'error' }), + new winston.transports.File({ filename: 'appdata/logs/combined.log' }), + new winston.transports.Console({level: !debugMode ? 'info' : 'debug', name: 'console'}) + ] +}); + +module.exports = logger; \ No newline at end of file diff --git a/backend/subscriptions.js b/backend/subscriptions.js index 89f0ff69..ece25f68 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -1,27 +1,20 @@ -const FileSync = require('lowdb/adapters/FileSync') +const fs = require('fs-extra'); +const path = require('path'); +const youtubedl = require('youtube-dl'); -var fs = require('fs-extra'); -const { uuid } = require('uuidv4'); -var path = require('path'); - -var youtubedl = require('youtube-dl'); const config_api = require('./config'); const twitch_api = require('./twitch'); -var utils = require('./utils'); +const utils = require('./utils'); +const logger = require('./logger'); const debugMode = process.env.YTDL_MODE === 'debug'; -var logger = null; -var db = null; -var users_db = null; let db_api = null; function setDB(input_db_api) { db_api = input_db_api } -function setLogger(input_logger) { logger = input_logger; } -function initialize(input_db_api, input_logger) { +function initialize(input_db_api) { setDB(input_db_api); - setLogger(input_logger); } async function subscribe(sub, user_uid = null) { @@ -52,7 +45,7 @@ async function subscribe(sub, user_uid = null) { getVideosForSub(sub, user_uid); } else { logger.error('Subscribe: Failed to get subscription info. Subscribe failed.') - }; + } result_obj.success = success; result_obj.sub = sub; @@ -146,7 +139,6 @@ async function unsubscribe(sub, deleteMode, user_uid = null) { basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions'); else basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); - let result_obj = { success: false, error: '' }; let id = sub.id; @@ -451,39 +443,26 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de } async function handleOutputJSON(sub, output_json, multiUserMode = null, reset_videos = false) { - // TODO: remove streaming only mode - if (false && sub.streamingOnly) { - if (reset_videos) { - sub_db.assign({videos: []}).write(); - } + const path_object = path.parse(output_json['_filename']); + const path_string = path.format(path_object); - // remove unnecessary info - output_json.formats = null; + const file_exists = await db_api.getRecord('files', {path: path_string, sub_id: sub.id}); + if (file_exists) { + // TODO: fix issue where files of different paths due to custom path get downloaded multiple times + // file already exists in DB, return early to avoid reseting the download date + return; + } - // add to db - sub_db.get('videos').push(output_json).write(); - } else { - path_object = path.parse(output_json['_filename']); - const path_string = path.format(path_object); + await db_api.registerFileDB2(output_json['_filename'], sub.type, sub.user_uid, null, sub.id); - const file_exists = await db_api.getRecord('files', {path: path_string, sub_id: sub.id}); - if (file_exists) { - // TODO: fix issue where files of different paths due to custom path get downloaded multiple times - // file already exists in DB, return early to avoid reseting the download date - return; - } - - await db_api.registerFileDB2(output_json['_filename'], sub.type, sub.user_uid, null, sub.id); - - const url = output_json['webpage_url']; - if (sub.type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1 - && config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) { - const file_name = path.basename(output_json['_filename']); - const id = file_name.substring(0, file_name.length-4); - let vodId = url.split('twitch.tv/videos/')[1]; - vodId = vodId.split('?')[0]; - twitch_api.downloadTwitchChatByVODID(vodId, id, sub.type, multiUserMode.user, sub); - } + const url = output_json['webpage_url']; + if (sub.type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1 + && config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) { + const file_name = path.basename(output_json['_filename']); + const id = file_name.substring(0, file_name.length-4); + let vodId = url.split('twitch.tv/videos/')[1]; + vodId = vodId.split('?')[0]; + twitch_api.downloadTwitchChatByVODID(vodId, id, sub.type, multiUserMode.user, sub); } } @@ -505,7 +484,7 @@ async function getSubscriptionByName(subName, user_uid = null) { return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid}); } -async function updateSubscription(sub, user_uid = null) { +async function updateSubscription(sub) { await db_api.updateRecord('subscriptions', {id: sub.id}, sub); return true; } @@ -516,7 +495,7 @@ async function updateSubscriptionPropertyMultiple(subs, assignment_obj) { }); } -async function updateSubscriptionProperty(sub, assignment_obj, user_uid = null) { +async function updateSubscriptionProperty(sub, assignment_obj) { // TODO: combine with updateSubscription await db_api.updateRecord('subscriptions', {id: sub.id}, assignment_obj); return true; @@ -585,7 +564,6 @@ module.exports = { unsubscribe : unsubscribe, deleteSubscriptionFile : deleteSubscriptionFile, getVideosForSub : getVideosForSub, - setLogger : setLogger, initialize : initialize, updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple } diff --git a/backend/utils.js b/backend/utils.js index 9370cf46..f37db820 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -1,6 +1,8 @@ const fs = require('fs-extra') const path = require('path') +const ffmpeg = require('fluent-ffmpeg'); const config_api = require('./config'); +const logger = require('./logger'); const CONSTS = require('./consts') const archiver = require('archiver'); @@ -349,6 +351,33 @@ function removeFileExtension(filename) { return filename_parts.join('.'); } +// ffmpeg helper functions + +async function cropFile(file_path, start, end, ext) { + return new Promise(resolve => { + const temp_file_path = `${file_path}.cropped${ext}`; + let base_ffmpeg_call = ffmpeg(file_path); + if (start) { + base_ffmpeg_call = base_ffmpeg_call.seekOutput(start); + } + if (end) { + base_ffmpeg_call = base_ffmpeg_call.duration(end - start); + } + base_ffmpeg_call + .on('end', () => { + logger.verbose(`Cropping for '${file_path}' complete.`); + fs.unlinkSync(file_path); + fs.moveSync(temp_file_path, file_path); + resolve(true); + }) + .on('error', (err) => { + logger.error(`Failed to crop ${file_path}.`); + logger.error(err); + resolve(false); + }).save(temp_file_path); + }); +} + /** * setTimeout, but its a promise. * @param {number} ms @@ -390,7 +419,7 @@ module.exports = { fixVideoMetadataPerms2: fixVideoMetadataPerms2, deleteJSONFile: deleteJSONFile, deleteJSONFile2: deleteJSONFile2, - removeIDFromArchive, removeIDFromArchive, + removeIDFromArchive: removeIDFromArchive, getDownloadedFilesByType: getDownloadedFilesByType, createContainerZipFile: createContainerZipFile, durationStringToNumber: durationStringToNumber, @@ -399,6 +428,7 @@ module.exports = { getCurrentDownloader: getCurrentDownloader, recFindByExt: recFindByExt, removeFileExtension: removeFileExtension, + cropFile: cropFile, wait: wait, File: File } diff --git a/package-lock.json b/package-lock.json index 0515bcab..2ac2dfac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1651,6 +1651,85 @@ } } }, + "@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz", + "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, "@istanbuljs/schema": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", @@ -1884,6 +1963,201 @@ } } }, + "@typescript-eslint/eslint-plugin": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.0.tgz", + "integrity": "sha512-eiREtqWRZ8aVJcNru7cT/AMVnYd9a2UHsfZT8MR1dW3UUEg6jDv9EQ9Cq4CUPZesyQ58YUpoAADGv71jY8RwgA==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.29.0", + "@typescript-eslint/scope-manager": "4.29.0", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.0.tgz", + "integrity": "sha512-FpNVKykfeaIxlArLUP/yQfv/5/3rhl1ov6RWgud4OgbqWLkEq7lqgQU9iiavZRzpzCRQV4XddyFz3wFXdkiX9w==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.29.0", + "@typescript-eslint/types": "4.29.0", + "@typescript-eslint/typescript-estree": "4.29.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.29.0.tgz", + "integrity": "sha512-+92YRNHFdXgq+GhWQPT2bmjX09X7EH36JfgN2/4wmhtwV/HPxozpCNst8jrWcngLtEVd/4zAwA6BKojAlf+YqA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "4.29.0", + "@typescript-eslint/types": "4.29.0", + "@typescript-eslint/typescript-estree": "4.29.0", + "debug": "^4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.29.0.tgz", + "integrity": "sha512-HPq7XAaDMM3DpmuijxLV9Io8/6pQnliiXMQUcAdjpJJSR+fdmbD/zHCd7hMkjJn04UQtCQBtshgxClzg6NIS2w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.29.0", + "@typescript-eslint/visitor-keys": "4.29.0" + } + }, + "@typescript-eslint/types": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.29.0.tgz", + "integrity": "sha512-2YJM6XfWfi8pgU2HRhTp7WgRw78TCRO3dOmSpAvIQ8MOv4B46JD2chnhpNT7Jq8j0APlIbzO1Bach734xxUl4A==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.0.tgz", + "integrity": "sha512-8ZpNHDIOyqzzgZrQW9+xQ4k5hM62Xy2R4RPO3DQxMc5Rq5QkCdSpk/drka+DL9w6sXNzV5nrdlBmf8+x495QXQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.29.0", + "@typescript-eslint/visitor-keys": "4.29.0", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.0.tgz", + "integrity": "sha512-LoaofO1C/jAJYs0uEpYMXfHboGXzOJeV118X4OsZu9f7rG7Pr9B3+4HTU8+err81rADa4xfQmAxnRnPAI2jp+Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.29.0", + "eslint-visitor-keys": "^2.0.0" + } + }, "@videogular/ngx-videogular": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@videogular/ngx-videogular/-/ngx-videogular-2.1.0.tgz", @@ -2117,6 +2391,12 @@ "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, "adjust-sourcemap-loader": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz", @@ -2405,6 +2685,12 @@ "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", "dev": true }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "async": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", @@ -4144,6 +4430,12 @@ "regexp.prototype.flags": "^1.2.0" } }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, "default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", @@ -4402,6 +4694,15 @@ "buffer-indexof": "^1.0.0" } }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", @@ -4684,6 +4985,15 @@ } } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "ent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", @@ -4827,6 +5137,249 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz", + "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -4837,12 +5390,69 @@ "estraverse": "^4.1.1" } }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, "esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -5220,6 +5830,12 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, "fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", @@ -5268,6 +5884,15 @@ "escape-string-regexp": "^1.0.5" } }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, "file-loader": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.1.1.tgz", @@ -5427,6 +6052,24 @@ "resolved": "https://registry.npmjs.org/fingerprintjs2/-/fingerprintjs2-2.1.0.tgz", "integrity": "sha512-H1k/ESTD2rJ3liupyqWBPjZC+LKfCGixQzz/NDN4dkgbmG1bVFyMOh7luKSkVDoyfhgvRm62pviNMPI+eJTZcQ==" }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "dependencies": { + "flatted": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", + "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", + "dev": true + } + } + }, "flatted": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", @@ -5553,6 +6196,12 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "genfun": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz", @@ -7008,6 +7657,12 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -7414,6 +8069,16 @@ } } }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, "license-webpack-plugin": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.3.1.tgz", @@ -7489,12 +8154,30 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -8158,6 +8841,12 @@ "dev": true, "optional": true }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -8674,6 +9363,20 @@ } } }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, "ora": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ora/-/ora-5.1.0.tgz", @@ -9953,6 +10656,12 @@ "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", "dev": true }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -10680,6 +11389,12 @@ } } }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, "regjsgen": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", @@ -10762,6 +11477,12 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -11517,6 +12238,43 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, "smart-buffer": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", @@ -12379,6 +13137,40 @@ "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==", "dev": true }, + "table": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "tapable": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", @@ -12815,6 +13607,15 @@ "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", "dev": true }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, "type-fest": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", @@ -13138,6 +13939,12 @@ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, "v8flags": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", @@ -14386,6 +15193,12 @@ "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", diff --git a/package.json b/package.json index a1c04827..ad1f6f99 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,11 @@ "@types/file-saver": "^2.0.1", "@types/jasmine": "~3.6.0", "@types/node": "^12.11.1", + "@typescript-eslint/eslint-plugin": "^4.29.0", + "@typescript-eslint/parser": "^4.29.0", "codelyzer": "^6.0.0", "electron": "^8.0.1", + "eslint": "^7.32.0", "jasmine-core": "~3.6.0", "jasmine-spec-reporter": "~5.0.0", "karma": "~5.0.0", diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index bcbb1830..9de15ae4 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -4,7 +4,6 @@ import { PostsService } from 'app/posts.services'; import { ActivatedRoute, Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { InputDialogComponent } from 'app/input-dialog/input-dialog.component'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component'; import { TwitchChatComponent } from 'app/components/twitch-chat/twitch-chat.component'; From 0360469c5abd460f7e7850316f4b474dcae6396c Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 8 Aug 2021 21:29:31 -0600 Subject: [PATCH 04/27] Download manager is now functional Added UI support for new downloads schema Implemented draft test for downloads Cleaned up unused code snippets --- backend/app.js | 31 ++-- backend/authentication/auth.js | 2 +- backend/db.js | 4 + backend/downloader.js | 254 ++++++++++++++++++++++------- backend/test/tests.js | 73 +++++---- backend/utils.js | 4 - src/app/main/main.component.html | 2 +- src/app/main/main.component.ts | 71 ++++---- src/app/player/player.component.ts | 16 -- src/app/posts.services.ts | 13 +- 10 files changed, 288 insertions(+), 182 deletions(-) diff --git a/backend/app.js b/backend/app.js index 3154f051..5ebdbec2 100644 --- a/backend/app.js +++ b/backend/app.js @@ -30,10 +30,11 @@ if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN const youtubedl = require('youtube-dl'); const logger = require('./logger'); -var config_api = require('./config.js'); -var subscriptions_api = require('./subscriptions') -var categories_api = require('./categories'); -var twitch_api = require('./twitch'); +const config_api = require('./config.js'); +const downloader_api = require('./downloader'); +const subscriptions_api = require('./subscriptions'); +const categories_api = require('./categories'); +const twitch_api = require('./twitch'); const is_windows = process.platform === 'win32'; @@ -64,6 +65,7 @@ const admin_token = '4241b401-7236-493e-92b5-b72696b9d853'; config_api.initialize(); db_api.initialize(db, users_db); auth_api.initialize(db_api); +downloader_api.initialize(db_api); subscriptions_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 } - let result_obj = await downloadFileByURL_exec(url, type, options, req.query.sessionID); - if (result_obj) { - res.send(result_obj); + const download = await downloader_api.createDownload(url, type, options); + + if (download) { + res.send({download: download}); } else { res.sendStatus(500); } @@ -2294,18 +2297,12 @@ app.get('/api/thumbnail/:path', optionalJwt, async (req, res) => { }); app.post('/api/download', async (req, res) => { - const session_id = req.body.session_id; - 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; + const download_uid = req.body.download_uid; - // find download - if (session_downloads && Object.keys(session_downloads)) { - found_download = Object.values(session_downloads).find(session_download => session_download['ui_uid'] === download_id); - } + const download = await db_api.getRecord('download_queue', {uid: download_uid}); - if (found_download) { - res.send({download: found_download}); + if (download) { + res.send({download: download}); } else { res.send({download: null}); } diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index caea4dae..7aad0701 100644 --- a/backend/authentication/auth.js +++ b/backend/authentication/auth.js @@ -134,7 +134,7 @@ exports.registerUser = async function(req, res) { exports.login = async (username, password) => { 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 } return await bcrypt.compare(password, user.passhash) ? user : false; } diff --git a/backend/db.js b/backend/db.js index 74a92c52..4ea0bb23 100644 --- a/backend/db.js +++ b/backend/db.js @@ -42,6 +42,10 @@ const tables = { name: 'roles', primary_key: 'key' }, + download_queue: { + name: 'download_queue', + primary_key: 'uid' + }, test: { name: 'test' } diff --git a/backend/downloader.js b/backend/downloader.js index 5054e060..e0b2778c 100644 --- a/backend/downloader.js +++ b/backend/downloader.js @@ -1,58 +1,114 @@ const fs = require('fs-extra'); const { uuid } = require('uuidv4'); 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 logger = require('./logger'); const config_api = require('./config'); const twitch_api = require('./twitch'); +const categories_api = require('./categories'); const utils = require('./utils'); 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 setLogger(input_logger) { logger = input_logger; } -exports.initialize = (input_db_api, input_logger) => { +exports.initialize = (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 = () => { } +// 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() { + logger.verbose('Checking downloads'); const downloads = await db_api.getRecords('download_queue'); downloads.sort((download1, download2) => download1.timestamp_start - download2.timestamp_start); - downloads = downloads.filter(download => !download.paused); - for (let i = 0; i < downloads.length; i++) { - if (i === config_api.getConfigItem('ytdl_')) + const running_downloads = downloads.filter(download => !download.paused); + for (let i = 0; i < running_downloads.length; i++) { + 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) { - 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 type = download['type']; 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 - const info = await getVideoInfoByURL(url, args); + let info = await getVideoInfoByURL(url, args, download_uid); if (!info) { // 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 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.noRelativePath = true; args = await generateArgs(url, type, options); - info = await getVideoInfoByURL(url, args); - - // must update args - await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args}); + info = await getVideoInfoByURL(url, args, download_uid); } - 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 downloadFileByURL_exec(url, type, options) { - return new Promise(resolve => { - const download = db_api.getRecord('download_queue', {uid: download_uid}); +async function downloadQueuedFile(download_uid) { + logger.verbose(`Downloading ${download_uid}`); + return new Promise(async resolve => { + const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path'); + const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path'); + await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false}); + const download = await db_api.getRecord('download_queue', {uid: download_uid}); const url = download['url']; const type = download['type']; @@ -84,20 +155,22 @@ async function downloadFileByURL_exec(url, type, options) { const args = download['args']; const category = download['category']; let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix - if (options.user) { - let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); - const user_path = path.join(usersFileFolder, options.user, type); - fs.ensureDirSync(user_path); - fileFolderPath = user_path + path.sep; - options.customFileFolderPath = fileFolderPath; + if (options.customFileFolderPath) { + fileFolderPath = options.customFileFolderPath; } + fs.ensureDirSync(fileFolderPath); + + const start_time = Date.now(); + + const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000); // download file youtubedl.exec(url, args, {maxBuffer: Infinity}, async function(err, output) { const file_objs = []; - let new_date = Date.now(); - let difference = (new_date - date)/1000; - logger.debug(`${is_audio ? 'Audio' : 'Video'} download delay: ${difference} seconds.`); + let end_time = Date.now(); + let difference = (end_time - start_time)/1000; + logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`); + clearInterval(download_checker); if (err) { logger.error(err.stderr); @@ -107,7 +180,6 @@ async function downloadFileByURL_exec(url, type, options) { if (output.length === 0 || output[0].length === 0) { // ERROR! logger.warn(`No output received for video download, check if it exists in your archive.`) - resolve(false); return; } @@ -127,11 +199,12 @@ async function downloadFileByURL_exec(url, type, options) { // get filepath with no extension const filepath_no_extension = utils.removeFileExtension(output_json['_filename']); + const ext = type === 'audio' ? '.mp3' : '.mp4'; var full_file_path = filepath_no_extension + ext; var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length); if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1 - && config.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]; vodId = vodId.split('?')[0]; 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']); logger.info('Renamed ' + file_name + '.webm to ' + file_name); } catch(e) { + } } @@ -156,7 +230,7 @@ async function downloadFileByURL_exec(url, type, options) { } 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 @@ -184,10 +258,9 @@ async function downloadFileByURL_exec(url, type, options) { logger.error('Downloaded file failed to result in metadata object.'); } - resolve({ - file_uids: file_objs.map(file_obj => file_obj.uid), - container: container - }); + const 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}); + resolve(); } }); }); @@ -196,17 +269,20 @@ async function downloadFileByURL_exec(url, type, options) { // helper functions 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 globalArgs = config_api.getConfigItem('ytdl_custom_args'); const useCookies = config_api.getConfigItem('ytdl_use_cookies'); const is_audio = type === 'audio'; - const fileFolderPath = is_audio ? audioFolderPath : videoFolderPath; + let fileFolderPath = is_audio ? audioFolderPath : videoFolderPath; if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath; const customArgs = options.customArgs; - const customOutput = options.customOutput; + let customOutput = options.customOutput; const customQualityConfiguration = options.customQualityConfiguration; // 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']; } - if (qualityPath && options.downloading_method === 'exec') downloadConfig.push(...qualityPath); + if (qualityPath) downloadConfig.push(...qualityPath); if (is_audio && !options.skip_audio_args) { 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) { downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent); } @@ -284,7 +362,7 @@ async function generateArgs(url, type, options) { await fs.ensureFile(merged_path); // merges blacklist and regular archive 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"); @@ -324,7 +402,7 @@ async function generateArgs(url, type, options) { return downloadConfig; } -async function getVideoInfoByURL(url, args = [], download = null) { +async function getVideoInfoByURL(url, args = [], download_uid = null) { return new Promise(resolve => { // remove bad args const new_args = [...args]; @@ -334,21 +412,85 @@ async function getVideoInfoByURL(url, args = [], download = null) { new_args.splice(archiveArgIndex, 2); } - // actually get info - youtubedl.getInfo(url, new_args, (err, output) => { + new_args.push('--dump-json'); + + youtubedl.exec(url, new_args, {maxBuffer: Infinity}, async (err, 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 { logger.error(`Error while retrieving info on video with URL ${url} with the following message: ${err}`); if (err.stderr) { logger.error(`${err.stderr}`) } - if (download) { - download['error'] = `Failed pre-check for video info: ${err}`; - updateDownloads(); + 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); } }); }); } + +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: '