diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..3879838 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 12 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['javascript'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/README.md b/README.md index 65486db..1c22952 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ environment: ## API -[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio) +[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml) To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing. @@ -111,6 +111,7 @@ If you're interested in translating the app into a new language, check out the [ Official translators: * Spanish - tzahi12345 * German - UnlimitedCookies +* Chinese - TyRoyal See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project. diff --git a/backend/app.js b/backend/app.js index 50af52e..19331f0 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,6 +1,6 @@ -var async = require('async'); 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'); @@ -18,7 +18,6 @@ var utils = require('./utils') var mergeFiles = require('merge-files'); const low = require('lowdb') var ProgressBar = require('progress'); -var md5 = require('md5'); const NodeID3 = require('node-id3') const downloader = require('youtube-dl/lib/downloader') const fetch = require('node-fetch'); @@ -194,55 +193,60 @@ app.use(auth_api.passport.initialize()); // actual functions -async function checkMigrations() { - return new Promise(async resolve => { - // 3.5->3.6 migration - const files_to_db_migration_complete = true; // migration phased out! previous code: db.get('files_to_db_migration_complete').value(); - - if (!files_to_db_migration_complete) { - logger.info('Beginning migration: 3.5->3.6+') - runFilesToDBMigration().then(success => { - if (success) { logger.info('3.5->3.6+ migration complete!'); } - else { logger.error('Migration failed: 3.5->3.6+'); } - }); - } - - resolve(true); +/** + * setTimeout, but its a promise. + * @param {number} ms + */ +async function wait(ms) { + await new Promise(resolve => { + setTimeout(resolve, ms); }); } +async function checkMigrations() { + // 3.5->3.6 migration + const files_to_db_migration_complete = true; // migration phased out! previous code: db.get('files_to_db_migration_complete').value(); + + if (!files_to_db_migration_complete) { + logger.info('Beginning migration: 3.5->3.6+') + const success = await runFilesToDBMigration() + if (success) { logger.info('3.5->3.6+ migration complete!'); } + else { logger.error('Migration failed: 3.5->3.6+'); } + } + + return true; +} + async function runFilesToDBMigration() { - return new Promise(async resolve => { - try { - let mp3s = getMp3s(); - let mp4s = getMp4s(); + try { + let mp3s = await getMp3s(); + let mp4s = await getMp4s(); - for (let i = 0; i < mp3s.length; i++) { - let file_obj = mp3s[i]; - const file_already_in_db = db.get('files.audio').find({id: file_obj.id}).value(); - if (!file_already_in_db) { - logger.verbose(`Migrating file ${file_obj.id}`); - db_api.registerFileDB(file_obj.id + '.mp3', 'audio'); - } + for (let i = 0; i < mp3s.length; i++) { + let file_obj = mp3s[i]; + const file_already_in_db = db.get('files.audio').find({id: file_obj.id}).value(); + if (!file_already_in_db) { + logger.verbose(`Migrating file ${file_obj.id}`); + await db_api.registerFileDB(file_obj.id + '.mp3', 'audio'); } - - for (let i = 0; i < mp4s.length; i++) { - let file_obj = mp4s[i]; - const file_already_in_db = db.get('files.video').find({id: file_obj.id}).value(); - if (!file_already_in_db) { - logger.verbose(`Migrating file ${file_obj.id}`); - db_api.registerFileDB(file_obj.id + '.mp4', 'video'); - } - } - - // sets migration to complete - db.set('files_to_db_migration_complete', true).write(); - resolve(true); - } catch(err) { - logger.error(err); - resolve(false); } - }); + + for (let i = 0; i < mp4s.length; i++) { + let file_obj = mp4s[i]; + const file_already_in_db = db.get('files.video').find({id: file_obj.id}).value(); + if (!file_already_in_db) { + logger.verbose(`Migrating file ${file_obj.id}`); + await db_api.registerFileDB(file_obj.id + '.mp4', 'video'); + } + } + + // sets migration to complete + db.set('files_to_db_migration_complete', true).write(); + return true; + } catch(err) { + logger.error(err); + return false; + } } async function startServer() { @@ -417,20 +421,20 @@ async function downloadReleaseZip(tag) { } async function installDependencies() { - return new Promise(resolve => { - var child_process = require('child_process'); - child_process.execSync('npm install',{stdio:[0,1,2]}); - resolve(true); - }); + var child_process = require('child_process'); + var exec = promisify(child_process.exec); + await exec('npm install',{stdio:[0,1,2]}); + return true; } async function backupServerLite() { - return new Promise(async resolve => { - fs.ensureDirSync(path.join(__dirname, 'appdata', 'backups')); - let output_path = path.join('appdata', 'backups', `backup-${Date.now()}.zip`); - logger.info(`Backing up your non-video/audio files to ${output_path}. This may take up to a few seconds/minutes.`); - let output = fs.createWriteStream(path.join(__dirname, output_path)); + await fs.ensureDir(path.join(__dirname, 'appdata', 'backups')); + let output_path = path.join('appdata', 'backups', `backup-${Date.now()}.zip`); + logger.info(`Backing up your non-video/audio files to ${output_path}. This may take up to a few seconds/minutes.`); + let output = fs.createWriteStream(path.join(__dirname, output_path)); + + await new Promise(resolve => { var archive = archiver('zip', { gzip: true, zlib: { level: 9 } // Sets the compression level. @@ -454,87 +458,80 @@ async function backupServerLite() { ignore: files_to_ignore }); - await archive.finalize(); - - // wait a tiny bit for the zip to reload in fs - setTimeout(function() { - resolve(true); - }, 100); + resolve(archive.finalize()); }); + + // wait a tiny bit for the zip to reload in fs + await wait(100); + return true; } async function isNewVersionAvailable() { - return new Promise(async resolve => { - // gets tag of the latest version of youtubedl-material, compare to current version - const latest_tag = await getLatestVersion(); - const current_tag = CONSTS['CURRENT_VERSION']; - if (latest_tag > current_tag) { - resolve(true); - } else { - resolve(false); - } - }); + // gets tag of the latest version of youtubedl-material, compare to current version + const latest_tag = await getLatestVersion(); + const current_tag = CONSTS['CURRENT_VERSION']; + if (latest_tag > current_tag) { + return true; + } else { + return false; + } } async function getLatestVersion() { - return new Promise(resolve => { - fetch('https://api.github.com/repos/tzahi12345/youtubedl-material/releases/latest', {method: 'Get'}) - .then(async res => res.json()) - .then(async (json) => { - if (json['message']) { - // means there's an error in getting latest version - logger.error(`ERROR: Received the following message from GitHub's API:`); - logger.error(json['message']); - if (json['documentation_url']) logger.error(`Associated URL: ${json['documentation_url']}`) - } - resolve(json['tag_name']); - return; - }); - }); + const res = await fetch('https://api.github.com/repos/tzahi12345/youtubedl-material/releases/latest', {method: 'Get'}); + const json = await res.json(); + + if (json['message']) { + // means there's an error in getting latest version + logger.error(`ERROR: Received the following message from GitHub's API:`); + logger.error(json['message']); + if (json['documentation_url']) logger.error(`Associated URL: ${json['documentation_url']}`) + } + return json['tag_name']; } async function killAllDownloads() { - return new Promise(resolve => { - ps.lookup({ - command: 'youtube-dl', - }, function(err, resultList ) { - if (err) { - // failed to get list of processes - logger.error('Failed to get a list of running youtube-dl processes.'); - logger.error(err); - resolve({ - details: err, - success: false - }); - } - - // processes that contain the string 'youtube-dl' in the name will be looped - resultList.forEach(function( process ){ - if (process) { - ps.kill(process.pid, 'SIGKILL', function( err ) { - if (err) { - // failed to kill, process may have ended on its own - logger.warn(`Failed to kill process with PID ${process.pid}`); - logger.warn(err); - } - else { - logger.verbose(`Process ${process.pid} has been killed!`); - } - }); + const lookupAsync = promisify(ps.lookup); + + try { + await lookupAsync({ + command: 'youtube-dl' + }); + } catch (err) { + // failed to get list of processes + logger.error('Failed to get a list of running youtube-dl processes.'); + logger.error(err); + return { + details: err, + success: false + }; + } + + // processes that contain the string 'youtube-dl' in the name will be looped + resultList.forEach(function( process ){ + if (process) { + ps.kill(process.pid, 'SIGKILL', function( err ) { + if (err) { + // failed to kill, process may have ended on its own + logger.warn(`Failed to kill process with PID ${process.pid}`); + logger.warn(err); + } + else { + logger.verbose(`Process ${process.pid} has been killed!`); } }); - resolve({ - success: true - }); - }); + } }); + + return { + success: true + }; } async function setPortItemFromENV() { - return new Promise(resolve => { - config_api.setConfigItem('ytdl_port', backendPort.toString()); - setTimeout(() => resolve(true), 100); - }); + config_api.setConfigItem('ytdl_port', backendPort.toString()); + await wait(100); + return true; } async function setAndLoadConfig() { @@ -543,51 +540,45 @@ async function setAndLoadConfig() { } async function setConfigFromEnv() { - return new Promise(resolve => { - let config_items = getEnvConfigItems(); - let success = config_api.setConfigItems(config_items); - if (success) { - logger.info('Config items set using ENV variables.'); - setTimeout(() => resolve(true), 100); - } else { - logger.error('ERROR: Failed to set config items using ENV variables.'); - resolve(false); - } - }); + let config_items = getEnvConfigItems(); + let success = config_api.setConfigItems(config_items); + if (success) { + logger.info('Config items set using ENV variables.'); + await wait(100); + return true; + } else { + logger.error('ERROR: Failed to set config items using ENV variables.'); + return false; + } } async function loadConfig() { - return new Promise(async resolve => { - loadConfigValues(); + loadConfigValues(); - // creates archive path if missing - if (!fs.existsSync(archivePath)){ - fs.mkdirSync(archivePath); - } + // creates archive path if missing + await fs.ensureDir(archivePath); - // get subscriptions - if (allowSubscriptions) { - // runs initially, then runs every ${subscriptionCheckInterval} seconds + // get subscriptions + if (allowSubscriptions) { + // runs initially, then runs every ${subscriptionCheckInterval} seconds + watchSubscriptions(); + setInterval(() => { watchSubscriptions(); - setInterval(() => { - watchSubscriptions(); - }, subscriptionsCheckInterval * 1000); - } + }, subscriptionsCheckInterval * 1000); + } - db_api.importUnregisteredFiles(); + db_api.importUnregisteredFiles(); - // check migrations - await checkMigrations(); + // check migrations + await checkMigrations(); - // load in previous downloads - downloads = db.get('downloads').value(); + // load in previous downloads + downloads = db.get('downloads').value(); - // start the server here - startServer(); - - resolve(true); - }); + // start the server here + startServer(); + return true; } function loadConfigValues() { @@ -704,17 +695,17 @@ function generateEnvVarConfigItem(key) { return {key: key, value: process['env'][key]}; } -function getMp3s() { +async function getMp3s() { let mp3s = []; - var files = utils.recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath); + var files = await utils.recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath); for (let i = 0; i < files.length; i++) { let file = files[i]; var file_path = file.substring(audioFolderPath.length, file.length); - var stats = fs.statSync(file); + var stats = await fs.stat(file); var id = file_path.substring(0, file_path.length-4); - var jsonobj = utils.getJSONMp3(id, audioFolderPath); + var jsonobj = await utils.getJSONMp3(id, audioFolderPath); if (!jsonobj) continue; var title = jsonobj.title; var url = jsonobj.webpage_url; @@ -733,9 +724,9 @@ function getMp3s() { return mp3s; } -function getMp4s(relative_path = true) { +async function getMp4s(relative_path = true) { let mp4s = []; - var files = utils.recFindByExt(videoFolderPath, 'mp4'); + var files = await utils.recFindByExt(videoFolderPath, 'mp4'); for (let i = 0; i < files.length; i++) { let file = files[i]; var file_path = file.substring(videoFolderPath.length, file.length); @@ -743,7 +734,7 @@ function getMp4s(relative_path = true) { var stats = fs.statSync(file); var id = file_path.substring(0, file_path.length-4); - var jsonobj = utils.getJSONMp4(id, videoFolderPath); + var jsonobj = await utils.getJSONMp4(id, videoFolderPath); if (!jsonobj) continue; var title = jsonobj.title; var url = jsonobj.webpage_url; @@ -848,260 +839,231 @@ function getVideoFormatID(name) } } -async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvided = null) { - return new Promise(async resolve => { - let zipFolderPath = null; +async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvided = null, user_uid = null) { + let zipFolderPath = null; - if (!fullPathProvided) { - zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath); - } else { - zipFolderPath = path.join(__dirname, config_api.getConfigItem('ytdl_subscriptions_base_path')); - } + if (!fullPathProvided) { + zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath); + if (user_uid) zipFolderPath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, zipFolderPath); + } else { + zipFolderPath = path.join(__dirname, config_api.getConfigItem('ytdl_subscriptions_base_path')); + } - let ext = (type === 'audio') ? '.mp3' : '.mp4'; + let ext = (type === 'audio') ? '.mp3' : '.mp4'; - let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip')); - - var archive = archiver('zip', { - gzip: true, - zlib: { level: 9 } // Sets the compression level. - }); - - archive.on('error', function(err) { - logger.error(err); - throw err; - }); - - // pipe archive data to the output file - archive.pipe(output); - - for (let i = 0; i < fileNames.length; i++) { - let fileName = fileNames[i]; - let fileNamePathRemoved = path.parse(fileName).base; - let file_path = !fullPathProvided ? zipFolderPath + fileName + ext : fileName; - archive.file(file_path, {name: fileNamePathRemoved + ext}) - } - - await archive.finalize(); - - // wait a tiny bit for the zip to reload in fs - setTimeout(function() { - resolve(path.join(zipFolderPath,outputName + '.zip')); - }, 100); + let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip')); + var archive = archiver('zip', { + gzip: true, + zlib: { level: 9 } // Sets the compression level. }); + archive.on('error', function(err) { + logger.error(err); + throw err; + }); + // pipe archive data to the output file + archive.pipe(output); + + for (let i = 0; i < fileNames.length; i++) { + let fileName = fileNames[i]; + let fileNamePathRemoved = path.parse(fileName).base; + let file_path = !fullPathProvided ? path.join(zipFolderPath, fileName + ext) : fileName; + archive.file(file_path, {name: fileNamePathRemoved + ext}) + } + + await archive.finalize(); + + // wait a tiny bit for the zip to reload in fs + await wait(100); + return path.join(zipFolderPath,outputName + '.zip'); } async function deleteAudioFile(name, customPath = null, blacklistMode = false) { - return new Promise(resolve => { - let filePath = customPath ? customPath : audioFolderPath; - - var jsonPath = path.join(filePath,name+'.mp3.info.json'); - var altJSONPath = path.join(filePath,name+'.info.json'); - var audioFilePath = path.join(filePath,name+'.mp3'); - var thumbnailPath = path.join(filePath,name+'.webp'); - var altThumbnailPath = path.join(filePath,name+'.jpg'); + let filePath = customPath ? customPath : audioFolderPath; - jsonPath = path.join(__dirname, jsonPath); - altJSONPath = path.join(__dirname, altJSONPath); - audioFilePath = path.join(__dirname, audioFilePath); + var jsonPath = path.join(filePath,name+'.mp3.info.json'); + var altJSONPath = path.join(filePath,name+'.info.json'); + var audioFilePath = path.join(filePath,name+'.mp3'); + var thumbnailPath = path.join(filePath,name+'.webp'); + var altThumbnailPath = path.join(filePath,name+'.jpg'); - let jsonExists = fs.existsSync(jsonPath); - let thumbnailExists = fs.existsSync(thumbnailPath); + jsonPath = path.join(__dirname, jsonPath); + altJSONPath = path.join(__dirname, altJSONPath); + audioFilePath = path.join(__dirname, audioFilePath); - if (!jsonExists) { - if (fs.existsSync(altJSONPath)) { - jsonExists = true; - jsonPath = altJSONPath; - } + let jsonExists = await fs.pathExists(jsonPath); + let thumbnailExists = await fs.pathExists(thumbnailPath); + + if (!jsonExists) { + if (await fs.pathExists(altJSONPath)) { + jsonExists = true; + jsonPath = altJSONPath; } + } - if (!thumbnailExists) { - if (fs.existsSync(altThumbnailPath)) { - thumbnailExists = true; - thumbnailPath = altThumbnailPath; - } + if (!thumbnailExists) { + if (await fs.pathExists(altThumbnailPath)) { + thumbnailExists = true; + thumbnailPath = altThumbnailPath; } + } - let audioFileExists = fs.existsSync(audioFilePath); - - if (config_api.descriptors[name]) { - try { - for (let i = 0; i < config_api.descriptors[name].length; i++) { - config_api.descriptors[name][i].destroy(); - } - } catch(e) { + let audioFileExists = await fs.pathExists(audioFilePath); + if (config_api.descriptors[name]) { + try { + for (let i = 0; i < config_api.descriptors[name].length; i++) { + config_api.descriptors[name][i].destroy(); } + } catch(e) { + } + } - let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); - if (useYoutubeDLArchive) { - const archive_path = path.join(archivePath, 'archive_audio.txt'); + let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); + if (useYoutubeDLArchive) { + const archive_path = path.join(archivePath, 'archive_audio.txt'); - // get ID from JSON + // get ID from JSON - var jsonobj = utils.getJSONMp3(name, filePath); - let id = null; - if (jsonobj) id = jsonobj.id; + var jsonobj = await utils.getJSONMp3(name, filePath); + let id = null; + if (jsonobj) id = jsonobj.id; - // use subscriptions API to remove video from the archive file, and write it to the blacklist - if (fs.existsSync(archive_path)) { - const line = id ? subscriptions_api.removeIDFromArchive(archive_path, id) : null; - if (blacklistMode && line) writeToBlacklist('audio', line); - } else { - logger.info('Could not find archive file for audio files. Creating...'); - fs.closeSync(fs.openSync(archive_path, 'w')); - } - } - - if (jsonExists) fs.unlinkSync(jsonPath); - if (thumbnailExists) fs.unlinkSync(thumbnailPath); - if (audioFileExists) { - fs.unlink(audioFilePath, function(err) { - if (fs.existsSync(jsonPath) || fs.existsSync(audioFilePath)) { - resolve(false); - } else { - resolve(true); - } - }); + // use subscriptions API to remove video from the archive file, and write it to the blacklist + if (await fs.pathExists(archive_path)) { + const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null; + if (blacklistMode && line) await writeToBlacklist('audio', line); } else { - // TODO: tell user that the file didn't exist - resolve(true); + logger.info('Could not find archive file for audio files. Creating...'); + await fs.close(await fs.open(archive_path, 'w')); } + } - }); + if (jsonExists) await fs.unlink(jsonPath); + if (thumbnailExists) await fs.unlink(thumbnailPath); + if (audioFileExists) { + await fs.unlink(audioFilePath); + if (await fs.pathExists(jsonPath) || await fs.pathExists(audioFilePath)) { + return false; + } else { + return true; + } + } else { + // TODO: tell user that the file didn't exist + return true; + } } async function deleteVideoFile(name, customPath = null, blacklistMode = false) { - return new Promise(resolve => { - let filePath = customPath ? customPath : videoFolderPath; - var jsonPath = path.join(filePath,name+'.info.json'); + let filePath = customPath ? customPath : videoFolderPath; + var jsonPath = path.join(filePath,name+'.info.json'); - var altJSONPath = path.join(filePath,name+'.mp4.info.json'); - var videoFilePath = path.join(filePath,name+'.mp4'); - var thumbnailPath = path.join(filePath,name+'.webp'); - var altThumbnailPath = path.join(filePath,name+'.jpg'); + var altJSONPath = path.join(filePath,name+'.mp4.info.json'); + var videoFilePath = path.join(filePath,name+'.mp4'); + var thumbnailPath = path.join(filePath,name+'.webp'); + var altThumbnailPath = path.join(filePath,name+'.jpg'); - jsonPath = path.join(__dirname, jsonPath); - videoFilePath = path.join(__dirname, videoFilePath); + jsonPath = path.join(__dirname, jsonPath); + videoFilePath = path.join(__dirname, videoFilePath); - let jsonExists = fs.existsSync(jsonPath); - let videoFileExists = fs.existsSync(videoFilePath); - let thumbnailExists = fs.existsSync(thumbnailPath); + let jsonExists = await fs.pathExists(jsonPath); + let videoFileExists = await fs.pathExists(videoFilePath); + let thumbnailExists = await fs.pathExists(thumbnailPath); - if (!jsonExists) { - if (fs.existsSync(altJSONPath)) { - jsonExists = true; - jsonPath = altJSONPath; - } + if (!jsonExists) { + if (await fs.pathExists(altJSONPath)) { + jsonExists = true; + jsonPath = altJSONPath; } - - if (!thumbnailExists) { - if (fs.existsSync(altThumbnailPath)) { - thumbnailExists = true; - thumbnailPath = altThumbnailPath; - } + } + + if (!thumbnailExists) { + if (await fs.pathExists(altThumbnailPath)) { + thumbnailExists = true; + thumbnailPath = altThumbnailPath; } + } - if (config_api.descriptors[name]) { - try { - for (let i = 0; i < config_api.descriptors[name].length; i++) { - config_api.descriptors[name][i].destroy(); - } - } catch(e) { - + if (config_api.descriptors[name]) { + try { + for (let i = 0; i < config_api.descriptors[name].length; i++) { + config_api.descriptors[name][i].destroy(); } + } catch(e) { + } + } - let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); - if (useYoutubeDLArchive) { - const archive_path = path.join(archivePath, 'archive_video.txt'); + let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); + if (useYoutubeDLArchive) { + const archive_path = path.join(archivePath, 'archive_video.txt'); - // get ID from JSON + // get ID from JSON - var jsonobj = utils.getJSONMp4(name, filePath); - let id = null; - if (jsonobj) id = jsonobj.id; + var jsonobj = await utils.getJSONMp4(name, filePath); + let id = null; + if (jsonobj) id = jsonobj.id; - // use subscriptions API to remove video from the archive file, and write it to the blacklist - if (fs.existsSync(archive_path)) { - const line = id ? subscriptions_api.removeIDFromArchive(archive_path, id) : null; - if (blacklistMode && line) writeToBlacklist('video', line); - } else { - logger.info('Could not find archive file for videos. Creating...'); - fs.closeSync(fs.openSync(archive_path, 'w')); - } - } - - if (jsonExists) fs.unlinkSync(jsonPath); - if (thumbnailExists) fs.unlinkSync(thumbnailPath); - if (videoFileExists) { - fs.unlink(videoFilePath, function(err) { - if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) { - resolve(false); - } else { - resolve(true); - } - }); + // use subscriptions API to remove video from the archive file, and write it to the blacklist + if (await fs.pathExists(archive_path)) { + const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null; + if (blacklistMode && line) await writeToBlacklist('video', line); } else { - // TODO: tell user that the file didn't exist - resolve(true); + logger.info('Could not find archive file for videos. Creating...'); + fs.closeSync(fs.openSync(archive_path, 'w')); + } + } + + if (jsonExists) await fs.unlink(jsonPath); + if (thumbnailExists) await fs.unlink(thumbnailPath); + if (videoFileExists) { + await fs.unlink(videoFilePath); + if (await fs.pathExists(jsonPath) || await fs.pathExists(videoFilePath)) { + return false; + } else { + return true; + } + } else { + // TODO: tell user that the file didn't exist + return true; + } +} + +/** + * @param {'audio' | 'video'} type + * @param {string[]} fileNames + */ +async function getAudioOrVideoInfos(type, fileNames) { + let result = await Promise.all(fileNames.map(async fileName => { + let fileLocation = videoFolderPath+fileName; + if (type === 'audio') { + fileLocation += '.mp3.info.json'; + } else if (type === 'video') { + fileLocation += '.info.json'; } - }); -} - -// replaces .webm with appropriate extension -function getTrueFileName(unfixed_path, type) { - let fixed_path = unfixed_path; - - const new_ext = (type === 'audio' ? 'mp3' : 'mp4'); - let unfixed_parts = unfixed_path.split('.'); - const old_ext = unfixed_parts[unfixed_parts.length-1]; - - - if (old_ext !== new_ext) { - unfixed_parts[unfixed_parts.length-1] = new_ext; - fixed_path = unfixed_parts.join('.'); - } - return fixed_path; -} - -function getAudioInfos(fileNames) { - let result = []; - for (let i = 0; i < fileNames.length; i++) { - let fileName = fileNames[i]; - let fileLocation = audioFolderPath+fileName+'.mp3.info.json'; - if (fs.existsSync(fileLocation)) { - let data = fs.readFileSync(fileLocation); + if (await fs.pathExists(fileLocation)) { + let data = await fs.readFile(fileLocation); try { - result.push(JSON.parse(data)); - } catch(e) { - logger.error(`Could not find info for file ${fileName}.mp3`); + return JSON.parse(data); + } catch (e) { + let suffix; + if (type === 'audio') { + suffix += '.mp3'; + } else if (type === 'video') { + suffix += '.mp4'; + } + + logger.error(`Could not find info for file ${fileName}${suffix}`); } } - } - return result; -} + return null; + })); -function getVideoInfos(fileNames) { - let result = []; - for (let i = 0; i < fileNames.length; i++) { - let fileName = fileNames[i]; - let fileLocation = videoFolderPath+fileName+'.info.json'; - if (fs.existsSync(fileLocation)) { - let data = fs.readFileSync(fileLocation); - try { - result.push(JSON.parse(data)); - } catch(e) { - logger.error(`Could not find info for file ${fileName}.mp4`); - } - } - } - return result; + return result.filter(data => data != null); } // downloads @@ -1414,134 +1376,131 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) { } async function generateArgs(url, type, options) { - return new Promise(async resolve => { - var videopath = '%(title)s'; - var globalArgs = config_api.getConfigItem('ytdl_custom_args'); - let useCookies = config_api.getConfigItem('ytdl_use_cookies'); - var is_audio = type === 'audio'; + var videopath = '%(title)s'; + var globalArgs = config_api.getConfigItem('ytdl_custom_args'); + let useCookies = config_api.getConfigItem('ytdl_use_cookies'); + var is_audio = type === 'audio'; - var fileFolderPath = is_audio ? audioFolderPath : videoFolderPath; + var fileFolderPath = is_audio ? audioFolderPath : videoFolderPath; - if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath; + if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath; - var customArgs = options.customArgs; - var customOutput = options.customOutput; - var customQualityConfiguration = options.customQualityConfiguration; + var customArgs = options.customArgs; + var customOutput = options.customOutput; + var customQualityConfiguration = options.customQualityConfiguration; - // video-specific args - var selectedHeight = options.selectedHeight; + // video-specific args + var selectedHeight = options.selectedHeight; - // audio-specific args - var maxBitrate = options.maxBitrate; + // audio-specific args + var maxBitrate = options.maxBitrate; - var youtubeUsername = options.youtubeUsername; - var youtubePassword = options.youtubePassword; + var youtubeUsername = options.youtubeUsername; + var 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'] + 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 (maxBitrate && is_audio) { + qualityPath = ['--audio-quality', maxBitrate] } - if (customArgs) { - downloadConfig = customArgs.split(',,'); + if (customOutput) { + downloadConfig = ['-o', path.join(fileFolderPath, customOutput) + ".%(ext)s", '--write-info-json', '--print-json']; } else { - if (customQualityConfiguration) { - qualityPath = ['-f', customQualityConfiguration]; - } else if (selectedHeight && selectedHeight !== '' && !is_audio) { - qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`]; - } else if (maxBitrate && is_audio) { - qualityPath = ['--audio-quality', maxBitrate] - } - - 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 (fs.existsSync(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`); - - fs.ensureDirSync(archive_folder); - - // create archive file if it doesn't exist - if (!fs.existsSync(archive_path)) { - fs.closeSync(fs.openSync(archive_path, 'w')); - } - - let blacklist_path = options.user ? path.join(fileFolderPath, 'archives', `blacklist_${type}.txt`) : path.join(archivePath, `blacklist_${type}.txt`); - // create blacklist file if it doesn't exist - if (!fs.existsSync(blacklist_path)) { - fs.closeSync(fs.openSync(blacklist_path, 'w')); - } - - let merged_path = path.join(fileFolderPath, `merged_${type}.txt`); - fs.ensureFileSync(merged_path); - // merges blacklist and regular archive - let inputPathList = [archive_path, blacklist_path]; - let status = await mergeFiles(inputPathList, merged_path); - - options.merged_string = fs.readFileSync(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(',,')); - } - + downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json']; } - logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`); - resolve(downloadConfig); - }); + + 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); + + // create archive file if it doesn't exist + if (!(await fs.pathExists(archive_path))) { + await fs.close(await fs.open(archive_path, 'w')); + } + + let blacklist_path = options.user ? path.join(fileFolderPath, 'archives', `blacklist_${type}.txt`) : path.join(archivePath, `blacklist_${type}.txt`); + // create blacklist file if it doesn't exist + if (!(await fs.pathExists(blacklist_path))) { + await fs.close(await fs.open(blacklist_path, 'w')); + } + + 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(',,')); + } + + } + 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 const new_args = [...args]; - + const archiveArgIndex = new_args.indexOf('--download-archive'); if (archiveArgIndex !== -1) { new_args.splice(archiveArgIndex, 2); @@ -1607,11 +1566,11 @@ async function convertFileToMp3(input_file, output_file) { }); } -function writeToBlacklist(type, line) { +async function writeToBlacklist(type, line) { let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt'); // adds newline to the beginning of the line line = '\n' + line; - fs.appendFileSync(blacklistPath, line); + await fs.appendFile(blacklistPath, line); } // download management functions @@ -1755,21 +1714,6 @@ function removeFileExtension(filename) { return filename_parts.join('.'); } -// https://stackoverflow.com/a/32197381/8088021 -const deleteFolderRecursive = function(folder_to_delete) { - if (fs.existsSync(folder_to_delete)) { - fs.readdirSync(folder_to_delete).forEach((file, index) => { - const curPath = path.join(folder_to_delete, file); - if (fs.lstatSync(curPath).isDirectory()) { // recurse - deleteFolderRecursive(curPath); - } else { // delete file - fs.unlinkSync(curPath); - } - }); - fs.rmdirSync(folder_to_delete); - } -}; - app.use(function(req, res, next) { res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization"); res.header("Access-Control-Allow-Origin", getOrigin()); @@ -1807,9 +1751,9 @@ const optionalJwt = function (req, res, next) { const uuid = using_body ? req.body.uuid : req.query.uuid; const uid = using_body ? req.body.uid : req.query.uid; const type = using_body ? req.body.type : req.query.type; - const file = !req.query.id ? auth_api.getUserVideo(uuid, uid, type, true, !!req.body) : auth_api.getUserPlaylist(uuid, req.query.id, null, true); - const is_shared = file ? file['sharingEnabled'] : false; - if (is_shared) { + const playlist_id = using_body ? req.body.id : req.query.id; + const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, type, true, req.body) : auth_api.getUserPlaylist(uuid, playlist_id, null, false); + if (file) { req.can_watch = true; return next(); } else { @@ -1891,7 +1835,7 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) { ui_uid: req.body.ui_uid, user: req.isAuthenticated() ? req.user.uid : null } - + const safeDownloadOverride = config_api.getConfigItem('ytdl_safe_download_override') || config_api.globalArgsRequiresSafeDownload(); if (safeDownloadOverride) logger.verbose('Download is running with the safe download override.'); const is_playlist = url.includes('playlist'); @@ -1913,8 +1857,21 @@ app.post('/api/killAllDownloads', optionalJwt, async function(req, res) { res.send(result_obj); }); +/** + * add thumbnails if present + * @param files - List of files with thumbnailPath property. + */ +async function addThumbnails(files) { + await Promise.all(files.map(async file => { + const thumbnailPath = file['thumbnailPath']; + if (thumbnailPath && (await fs.pathExists(thumbnailPath))) { + file['thumbnailBlob'] = await fs.readFile(thumbnailPath); + } + })); +} + // gets all download mp3s -app.get('/api/getMp3s', optionalJwt, function(req, res) { +app.get('/api/getMp3s', optionalJwt, async function(req, res) { var mp3s = db.get('files.audio').value(); // getMp3s(); var playlists = db.get('playlists.audio').value(); const is_authenticated = req.isAuthenticated(); @@ -1929,12 +1886,10 @@ app.get('/api/getMp3s', optionalJwt, function(req, res) { if (config_api.getConfigItem('ytdl_include_thumbnail')) { // add thumbnails if present - mp3s.forEach(mp3 => { - if (mp3['thumbnailPath'] && fs.existsSync(mp3['thumbnailPath'])) - mp3['thumbnailBlob'] = fs.readFileSync(mp3['thumbnailPath']); - }); + await addThumbnails(mp3s); } + res.send({ mp3s: mp3s, playlists: playlists @@ -1942,7 +1897,7 @@ app.get('/api/getMp3s', optionalJwt, function(req, res) { }); // gets all download mp4s -app.get('/api/getMp4s', optionalJwt, function(req, res) { +app.get('/api/getMp4s', optionalJwt, async function(req, res) { var mp4s = db.get('files.video').value(); // getMp4s(); var playlists = db.get('playlists.video').value(); @@ -1958,10 +1913,7 @@ app.get('/api/getMp4s', optionalJwt, function(req, res) { if (config_api.getConfigItem('ytdl_include_thumbnail')) { // add thumbnails if present - mp4s.forEach(mp4 => { - if (mp4['thumbnailPath'] && fs.existsSync(mp4['thumbnailPath'])) - mp4['thumbnailBlob'] = fs.readFileSync(mp4['thumbnailPath']); - }); + await addThumbnails(mp4s); } res.send({ @@ -2008,7 +1960,7 @@ app.post('/api/getFile', optionalJwt, function (req, res) { } }); -app.post('/api/getAllFiles', optionalJwt, function (req, res) { +app.post('/api/getAllFiles', optionalJwt, async function (req, res) { // these are returned let files = []; let playlists = []; @@ -2018,7 +1970,7 @@ app.post('/api/getAllFiles', optionalJwt, function (req, res) { let audios = null; let audio_playlists = null; let video_playlists = null; - let subscriptions = subscriptions_api.getAllSubscriptions(req.isAuthenticated() ? req.user.uid : null); + let subscriptions = config_api.getConfigItem('ytdl_allow_subscriptions') ? (subscriptions_api.getAllSubscriptions(req.isAuthenticated() ? req.user.uid : null)) : []; // get basic info depending on multi-user mode being enabled if (req.isAuthenticated()) { @@ -2035,7 +1987,7 @@ app.post('/api/getAllFiles', optionalJwt, function (req, res) { files = videos.concat(audios); playlists = video_playlists.concat(audio_playlists); - + // loop through subscriptions and add videos for (let i = 0; i < subscriptions.length; i++) { sub = subscriptions[i]; @@ -2052,12 +2004,9 @@ app.post('/api/getAllFiles', optionalJwt, function (req, res) { if (config_api.getConfigItem('ytdl_include_thumbnail')) { // add thumbnails if present - files.forEach(file => { - if (file['thumbnailPath'] && fs.existsSync(file['thumbnailPath'])) - file['thumbnailBlob'] = fs.readFileSync(file['thumbnailPath']); - }); + await addThumbnails(files); } - + res.send({ files: files, playlists: playlists @@ -2315,7 +2264,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => { let appended_base_path = path.join(base_path, (subscription.isPlaylist ? 'playlists' : 'channels'), subscription.name, '/'); let files; try { - files = utils.recFindByExt(appended_base_path, 'mp4'); + files = await utils.recFindByExt(appended_base_path, 'mp4'); } catch(e) { files = null; logger.info('Failed to get folder for subscription: ' + subscription.name + ' at path ' + appended_base_path); @@ -2539,7 +2488,7 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => { var name = file_obj.id; var fullpath = file_obj ? file_obj.path : null; var wasDeleted = false; - if (fs.existsSync(fullpath)) + if (await fs.pathExists(fullpath)) { wasDeleted = type === 'audio' ? await deleteAudioFile(name, path.basename(fullpath), blacklistMode) : await deleteVideoFile(name, path.basename(fullpath), blacklistMode); db.get('files.video').remove({uid: uid}).write(); @@ -2573,9 +2522,9 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => { let base_path = fileFolderPath; let usersFileFolder = null; const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); - if (multiUserMode && req.body.uuid) { + if (multiUserMode && (req.body.uuid || req.user.uid)) { usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); - base_path = path.join(usersFileFolder, req.body.uuid, type); + base_path = path.join(usersFileFolder, req.body.uuid ? req.body.uuid : req.user.uid, type); } if (!subscriptionName) { file = path.join(__dirname, base_path, fileNames + ext); @@ -2592,7 +2541,8 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => { for (let i = 0; i < fileNames.length; i++) { fileNames[i] = decodeURIComponent(fileNames[i]); } - file = await createPlaylistZipFile(fileNames, type, outputName, fullPathProvided); + file = await createPlaylistZipFile(fileNames, type, outputName, fullPathProvided, req.body.uuid); + if (!path.isAbsolute(file)) file = path.join(__dirname, file); } res.sendFile(file, function (err) { if (err) { @@ -2613,7 +2563,7 @@ app.post('/api/downloadArchive', async (req, res) => { let full_archive_path = path.join(archive_dir, 'archive.txt'); - if (fs.existsSync(full_archive_path)) { + if (await fs.pathExists(full_archive_path)) { res.sendFile(full_archive_path); } else { res.sendStatus(404); @@ -2625,14 +2575,14 @@ var upload_multer = multer({ dest: __dirname + '/appdata/' }); app.post('/api/uploadCookies', upload_multer.single('cookies'), async (req, res) => { const new_path = path.join(__dirname, 'appdata', 'cookies.txt'); - if (fs.existsSync(req.file.path)) { - fs.renameSync(req.file.path, new_path); + if (await fs.pathExists(req.file.path)) { + await fs.rename(req.file.path, new_path); } else { res.sendStatus(500); return; } - if (fs.existsSync(new_path)) { + if (await fs.pathExists(new_path)) { res.send({success: true}); } else { res.sendStatus(500); @@ -2806,9 +2756,9 @@ app.post('/api/logs', async function(req, res) { let logs = null; let lines = req.body.lines; logs_path = path.join('appdata', 'logs', 'combined.log') - if (fs.existsSync(logs_path)) { + if (await fs.pathExists(logs_path)) { if (lines) logs = await read_last_lines.read(logs_path, lines); - else logs = fs.readFileSync(logs_path, 'utf8'); + else logs = await fs.readFile(logs_path, 'utf8'); } else logger.error(`Failed to find logs file at the expected location: ${logs_path}`) @@ -2824,8 +2774,10 @@ app.post('/api/clearAllLogs', async function(req, res) { logs_err_path = path.join('appdata', 'logs', 'error.log'); let success = false; try { - fs.writeFileSync(logs_path, ''); - fs.writeFileSync(logs_err_path, ''); + await Promise.all([ + fs.writeFile(logs_path, ''), + fs.writeFile(logs_err_path, '') + ]) success = true; } catch(e) { logger.error(e); @@ -2842,10 +2794,8 @@ app.post('/api/clearAllLogs', async function(req, res) { let type = req.body.type; let result = null; if (!urlMode) { - if (type === 'audio') { - result = getAudioInfos(fileNames) - } else if (type === 'video') { - result = getVideoInfos(fileNames); + if (type === 'audio' || type === 'video') { + result = await getAudioOrVideoInfos(type, fileNames); } } else { result = await getUrlInfos(fileNames); @@ -2918,7 +2868,7 @@ app.post('/api/deleteUser', optionalJwt, async (req, res) => { const user_db_obj = users_db.get('users').find({uid: uid}); if (user_db_obj.value()) { // user exists, let's delete - deleteFolderRecursive(user_folder); + await fs.remove(user_folder); users_db.get('users').remove({uid: uid}).write(); } res.send({success: true}); diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index a64ca19..f16fbf1 100644 --- a/backend/authentication/auth.js +++ b/backend/authentication/auth.js @@ -139,12 +139,12 @@ exports.registerUser = function(req, res) { exports.passport.use(new LocalStrategy({ usernameField: 'username', passwordField: 'password'}, - function(username, password, done) { + async function(username, password, done) { const user = users_db.get('users').find({name: username}).value(); if (!user) { logger.error(`User ${username} not found`); return done(null, false); } if (user.auth_method && user.auth_method !== 'internal') { return done(null, false); } if (user) { - return done(null, bcrypt.compareSync(password, user.passhash) ? user : false); + return done(null, (await bcrypt.compare(password, user.passhash)) ? user : false); } } )); @@ -160,7 +160,7 @@ exports.passport.use(new LdapStrategy(getLDAPConfiguration, // check if ldap auth is enabled const ldap_enabled = config_api.getConfigItem('ytdl_auth_method') === 'ldap'; if (!ldap_enabled) return done(null, false); - + const user_uid = user.uid; let db_user = users_db.get('users').find({uid: user_uid}).value(); if (!db_user) { @@ -226,15 +226,13 @@ exports.ensureAuthenticatedElseError = function(req, res, next) { // change password exports.changeUserPassword = async function(user_uid, new_pass) { - return new Promise(resolve => { - bcrypt.hash(new_pass, saltRounds) - .then(function(hash) { - users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write(); - resolve(true); - }).catch(err => { - resolve(false); - }); - }); + try { + const hash = await bcrypt.hash(new_pass, saltRounds); + users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write(); + return true; + } catch (err) { + return false; + } } // change user permissions @@ -283,6 +281,7 @@ exports.getUserVideos = function(user_uid, type) { } exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) { + let file = null; if (!type) { file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value(); if (!file) { @@ -296,7 +295,7 @@ exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value(); // prevent unauthorized users from accessing the file info - if (requireSharing && !file['sharingEnabled']) file = null; + if (file && !file['sharingEnabled'] && requireSharing) file = null; return file; } @@ -351,7 +350,7 @@ exports.registerUserFile = function(user_uid, file_object, type) { .write(); } -exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) { +exports.deleteUserFile = async function(user_uid, file_uid, type, blacklistMode = false) { let success = false; const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value(); if (file_obj) { @@ -374,20 +373,20 @@ exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = fals .remove({ uid: file_uid }).write(); - if (fs.existsSync(full_path)) { + if (await fs.pathExists(full_path)) { // remove json and file const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json'); const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json'); let youtube_id = null; - if (fs.existsSync(json_path)) { - youtube_id = fs.readJSONSync(json_path).id; - fs.unlinkSync(json_path); - } else if (fs.existsSync(alternate_json_path)) { - youtube_id = fs.readJSONSync(alternate_json_path).id; - fs.unlinkSync(alternate_json_path); + if (await fs.pathExists(json_path)) { + youtube_id = await fs.readJSON(json_path).id; + await fs.unlink(json_path); + } else if (await fs.pathExists(alternate_json_path)) { + youtube_id = await fs.readJSON(alternate_json_path).id; + await fs.unlink(alternate_json_path); } - fs.unlinkSync(full_path); + await fs.unlink(full_path); // do archive stuff @@ -396,17 +395,17 @@ exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = fals const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`); // use subscriptions API to remove video from the archive file, and write it to the blacklist - if (fs.existsSync(archive_path)) { - const line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null; + if (await fs.pathExists(archive_path)) { + const line = youtube_id ? await subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null; if (blacklistMode && line) { let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`); // adds newline to the beginning of the line line = '\n' + line; - fs.appendFileSync(blacklistPath, line); + await fs.appendFile(blacklistPath, line); } } else { logger.info(`Could not find archive file for ${type} files. Creating...`); - fs.ensureFileSync(archive_path); + await fs.ensureFile(archive_path); } } } @@ -532,7 +531,7 @@ function generateUserObject(userid, username, hash, auth_method = 'internal') { role: userid === 'admin' && auth_method === 'internal' ? 'admin' : 'user', permissions: [], permission_overrides: [], - auth_method: auth_method + auth_method: auth_method }; return new_user; } diff --git a/backend/db.js b/backend/db.js index f625807..a2ddb7a 100644 --- a/backend/db.js +++ b/backend/db.js @@ -182,9 +182,9 @@ async function importUnregisteredFiles() { } // run through check list and check each file to see if it's missing from the db - dirs_to_check.forEach(dir_to_check => { + for (const dir_to_check of dirs_to_check) { // recursively get all files in dir's path - const files = utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type); + const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type); files.forEach(file => { // check if file exists in db, if not add it @@ -195,7 +195,7 @@ async function importUnregisteredFiles() { logger.verbose(`Added discovered file to the database: ${file.id}`); } }); - }); + } } diff --git a/backend/package-lock.json b/backend/package-lock.json index 1e4f7ab..c969112 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1968,9 +1968,9 @@ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, "node-id3": { "version": "0.1.16", diff --git a/backend/package.json b/backend/package.json index b153a5d..91a1777 100644 --- a/backend/package.json +++ b/backend/package.json @@ -43,7 +43,7 @@ "md5": "^2.2.1", "merge-files": "^0.1.2", "multer": "^1.4.2", - "node-fetch": "^2.6.0", + "node-fetch": "^2.6.1", "node-id3": "^0.1.14", "nodemon": "^2.0.2", "passport": "^0.4.1", diff --git a/backend/subscriptions.js b/backend/subscriptions.js index f1c20a7..5eafc4e 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -79,17 +79,18 @@ async function getSubscriptionInfo(sub, user_uid = null) { else basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); - return new Promise(resolve => { - // get videos - let downloadConfig = ['--dump-json', '--playlist-end', '1']; - let useCookies = config_api.getConfigItem('ytdl_use_cookies'); - if (useCookies) { - if (fs.existsSync(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.'); - } + // get videos + let downloadConfig = ['--dump-json', '--playlist-end', '1']; + let useCookies = config_api.getConfigItem('ytdl_use_cookies'); + 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.'); } + } + + return new Promise(resolve => { youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) { if (debugMode) { logger.info('Subscribe: got info for subscription ' + sub.id); @@ -152,39 +153,36 @@ async function getSubscriptionInfo(sub, user_uid = null) { } async function unsubscribe(sub, deleteMode, user_uid = null) { - return new Promise(async resolve => { - let basePath = null; - if (user_uid) - 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 basePath = null; + if (user_uid) + 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; - if (user_uid) - users_db.get('users').find({uid: user_uid}).get('subscriptions').remove({id: id}).write(); - else - db.get('subscriptions').remove({id: id}).write(); + let id = sub.id; + if (user_uid) + users_db.get('users').find({uid: user_uid}).get('subscriptions').remove({id: id}).write(); + else + db.get('subscriptions').remove({id: id}).write(); - // failed subs have no name, on unsubscribe they shouldn't error - if (!sub.name) { - return; - } + // failed subs have no name, on unsubscribe they shouldn't error + if (!sub.name) { + return; + } - const appendedBasePath = getAppendedBasePath(sub, basePath); - if (deleteMode && fs.existsSync(appendedBasePath)) { - if (sub.archive && fs.existsSync(sub.archive)) { - const archive_file_path = path.join(sub.archive, 'archive.txt'); - // deletes archive if it exists - if (fs.existsSync(archive_file_path)) { - fs.unlinkSync(archive_file_path); - } - fs.rmdirSync(sub.archive); + const appendedBasePath = getAppendedBasePath(sub, basePath); + if (deleteMode && (await fs.pathExists(appendedBasePath))) { + if (sub.archive && (await fs.pathExists(sub.archive))) { + const archive_file_path = path.join(sub.archive, 'archive.txt'); + // deletes archive if it exists + if (await fs.pathExists(archive_file_path)) { + await fs.unlink(archive_file_path); } - deleteFolderRecursive(appendedBasePath); + await fs.rmdir(sub.archive); } - }); - + await fs.remove(appendedBasePath); + } } async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) { @@ -202,155 +200,154 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, const name = file; let retrievedID = null; sub_db.get('videos').remove({uid: file_uid}).write(); - return new Promise(resolve => { - let filePath = appendedBasePath; - const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4' - var jsonPath = path.join(__dirname,filePath,name+'.info.json'); - var videoFilePath = path.join(__dirname,filePath,name+ext); - var imageFilePath = path.join(__dirname,filePath,name+'.jpg'); - var altImageFilePath = path.join(__dirname,filePath,name+'.jpg'); - jsonExists = fs.existsSync(jsonPath); - videoFileExists = fs.existsSync(videoFilePath); - imageFileExists = fs.existsSync(imageFilePath); - altImageFileExists = fs.existsSync(altImageFilePath); + let filePath = appendedBasePath; + const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4' + var jsonPath = path.join(__dirname,filePath,name+'.info.json'); + var videoFilePath = path.join(__dirname,filePath,name+ext); + var imageFilePath = path.join(__dirname,filePath,name+'.jpg'); + var altImageFilePath = path.join(__dirname,filePath,name+'.jpg'); - if (jsonExists) { - retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id']; - fs.unlinkSync(jsonPath); - } + const [jsonExists, videoFileExists, imageFileExists, altImageFileExists] = await Promise.all([ + fs.pathExists(jsonPath), + fs.pathExists(videoFilePath), + fs.pathExists(imageFilePath), + fs.pathExists(altImageFilePath), + ]); - if (imageFileExists) { - fs.unlinkSync(imageFilePath); - } + if (jsonExists) { + retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id']; + await fs.unlink(jsonPath); + } - if (altImageFileExists) { - fs.unlinkSync(altImageFilePath); - } + if (imageFileExists) { + await fs.unlink(imageFilePath); + } - if (videoFileExists) { - fs.unlink(videoFilePath, function(err) { - if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) { - resolve(false); - } else { - // check if the user wants the video to be redownloaded (deleteForever === false) - if (!deleteForever && useArchive && sub.archive && retrievedID) { - const archive_path = path.join(sub.archive, 'archive.txt') - // if archive exists, remove line with video ID - if (fs.existsSync(archive_path)) { - removeIDFromArchive(archive_path, retrievedID); - } - } - resolve(true); - } - }); + if (altImageFileExists) { + await fs.unlink(altImageFilePath); + } + + if (videoFileExists) { + await fs.unlink(videoFilePath); + if ((await fs.pathExists(jsonPath)) || (await fs.pathExists(videoFilePath))) { + return false; } else { - // TODO: tell user that the file didn't exist - resolve(true); + // check if the user wants the video to be redownloaded (deleteForever === false) + if (!deleteForever && useArchive && sub.archive && retrievedID) { + const archive_path = path.join(sub.archive, 'archive.txt') + // if archive exists, remove line with video ID + if (await fs.pathExists(archive_path)) { + await removeIDFromArchive(archive_path, retrievedID); + } + } + return true; } - - }); + } else { + // TODO: tell user that the file didn't exist + return true; + } } async function getVideosForSub(sub, user_uid = null) { - return new Promise(resolve => { - if (!subExists(sub.id, user_uid)) { - resolve(false); - return; + if (!subExists(sub.id, user_uid)) { + return false; + } + + // get sub_db + let sub_db = null; + if (user_uid) + sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}); + else + sub_db = db.get('subscriptions').find({id: sub.id}); + + // get basePath + let basePath = null; + if (user_uid) + basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions'); + else + basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); + + const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); + + let appendedBasePath = null + appendedBasePath = getAppendedBasePath(sub, basePath); + + let multiUserMode = null; + if (user_uid) { + multiUserMode = { + user: user_uid, + file_path: appendedBasePath } + } - // get sub_db - let sub_db = null; - if (user_uid) - sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}); - else - sub_db = db.get('subscriptions').find({id: sub.id}); + const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4' - // get basePath - let basePath = null; - if (user_uid) - basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions'); - else - basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); + let fullOutput = `${appendedBasePath}/%(title)s.%(ext)s`; + if (sub.custom_output) { + fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`; + } - const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); + let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json']; - let appendedBasePath = null - appendedBasePath = getAppendedBasePath(sub, basePath); + let qualityPath = null; + if (sub.type && sub.type === 'audio') { + qualityPath = ['-f', 'bestaudio'] + qualityPath.push('-x'); + qualityPath.push('--audio-format', 'mp3'); + } else { + qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'] + } - let multiUserMode = null; - if (user_uid) { - multiUserMode = { - user: user_uid, - file_path: appendedBasePath - } + downloadConfig.push(...qualityPath) + + if (sub.custom_args) { + customArgsArray = sub.custom_args.split(',,'); + if (customArgsArray.indexOf('-f') !== -1) { + // if custom args has a custom quality, replce the original quality with that of custom args + const original_output_index = downloadConfig.indexOf('-f'); + downloadConfig.splice(original_output_index, 2); } + downloadConfig.push(...customArgsArray); + } - const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4' + let archive_dir = null; + let archive_path = null; - let fullOutput = appendedBasePath + '/%(title)s' + ext; - if (sub.custom_output) { - fullOutput = appendedBasePath + '/' + sub.custom_output + ext; + if (useArchive) { + if (sub.archive) { + archive_dir = sub.archive; + archive_path = path.join(archive_dir, 'archive.txt') } + downloadConfig.push('--download-archive', archive_path); + } - let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json']; + // if streaming only mode, just get the list of videos + if (sub.streamingOnly) { + downloadConfig = ['-f', 'best', '--dump-json']; + } - let qualityPath = null; - if (sub.type && sub.type === 'audio') { - qualityPath = ['-f', 'bestaudio'] - qualityPath.push('-x'); - qualityPath.push('--audio-format', 'mp3'); + if (sub.timerange) { + downloadConfig.push('--dateafter', sub.timerange); + } + + let useCookies = config_api.getConfigItem('ytdl_use_cookies'); + if (useCookies) { + if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) { + downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt')); } else { - qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'] + logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.'); } + } - downloadConfig.push(...qualityPath) + if (config_api.getConfigItem('ytdl_include_thumbnail')) { + downloadConfig.push('--write-thumbnail'); + } - if (sub.custom_args) { - customArgsArray = sub.custom_args.split(',,'); - if (customArgsArray.indexOf('-f') !== -1) { - // if custom args has a custom quality, replce the original quality with that of custom args - const original_output_index = downloadConfig.indexOf('-f'); - downloadConfig.splice(original_output_index, 2); - } - downloadConfig.push(...customArgsArray); - } + // get videos + logger.verbose('Subscription: getting videos for subscription ' + sub.name); - let archive_dir = null; - let archive_path = null; - - if (useArchive) { - if (sub.archive) { - archive_dir = sub.archive; - archive_path = path.join(archive_dir, 'archive.txt') - } - downloadConfig.push('--download-archive', archive_path); - } - - // if streaming only mode, just get the list of videos - if (sub.streamingOnly) { - downloadConfig = ['-f', 'best', '--dump-json']; - } - - if (sub.timerange) { - downloadConfig.push('--dateafter', sub.timerange); - } - - let useCookies = config_api.getConfigItem('ytdl_use_cookies'); - if (useCookies) { - if (fs.existsSync(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 (config_api.getConfigItem('ytdl_include_thumbnail')) { - downloadConfig.push('--write-thumbnail'); - } - - // get videos - logger.verbose('Subscription: getting videos for subscription ' + sub.name); + return new Promise(resolve => { youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) { logger.verbose('Subscription: finished check for ' + sub.name); if (err && !output) { @@ -463,23 +460,8 @@ function getAppendedBasePath(sub, base_path) { return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name); } -// https://stackoverflow.com/a/32197381/8088021 -const deleteFolderRecursive = function(folder_to_delete) { - if (fs.existsSync(folder_to_delete)) { - fs.readdirSync(folder_to_delete).forEach((file, index) => { - const curPath = path.join(folder_to_delete, file); - if (fs.lstatSync(curPath).isDirectory()) { // recurse - deleteFolderRecursive(curPath); - } else { // delete file - fs.unlinkSync(curPath); - } - }); - fs.rmdirSync(folder_to_delete); - } - }; - -function removeIDFromArchive(archive_path, id) { - let data = fs.readFileSync(archive_path, {encoding: 'utf-8'}); +async function removeIDFromArchive(archive_path, id) { + let data = await fs.readFile(archive_path, {encoding: 'utf-8'}); if (!data) { logger.error('Archive could not be found.'); return; @@ -500,7 +482,7 @@ function removeIDFromArchive(archive_path, id) { // UPDATE FILE WITH NEW DATA const updatedData = dataArray.join('\n'); - fs.writeFileSync(archive_path, updatedData); + await fs.writeFile(archive_path, updatedData); if (line) return line; if (err) throw err; } diff --git a/backend/utils.js b/backend/utils.js index efedb3b..411a7da 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -4,6 +4,7 @@ const config_api = require('./config'); const is_windows = process.platform === 'win32'; +// replaces .webm with appropriate extension function getTrueFileName(unfixed_path, type) { let fixed_path = unfixed_path; @@ -19,21 +20,21 @@ function getTrueFileName(unfixed_path, type) { return fixed_path; } -function getDownloadedFilesByType(basePath, type) { +async function getDownloadedFilesByType(basePath, type) { // return empty array if the path doesn't exist - if (!fs.existsSync(basePath)) return []; + if (!(await fs.pathExists(basePath))) return []; let files = []; const ext = type === 'audio' ? 'mp3' : 'mp4'; - var located_files = recFindByExt(basePath, ext); + var located_files = await recFindByExt(basePath, ext); for (let i = 0; i < located_files.length; i++) { let file = located_files[i]; - var file_path = path.basename(file); + var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length); - var stats = fs.statSync(file); + var stats = await fs.stat(file); var id = file_path.substring(0, file_path.length-4); - var jsonobj = getJSONByType(type, id, basePath); + var jsonobj = await getJSONByType(type, id, basePath); if (!jsonobj) continue; var title = jsonobj.title; var url = jsonobj.webpage_url; @@ -129,7 +130,7 @@ function fixVideoMetadataPerms(name, type, customPath = null) { : config_api.getConfigItem('ytdl_video_folder_path'); const ext = type === 'audio' ? '.mp3' : '.mp4'; - + const files_to_fix = [ // JSONs path.join(customPath, name + '.info.json'), @@ -158,27 +159,25 @@ function deleteJSONFile(name, type, customPath = null) { } -function recFindByExt(base,ext,files,result) +async function recFindByExt(base,ext,files,result) { - files = files || fs.readdirSync(base) + files = files || (await fs.readdir(base)) result = result || [] - files.forEach( - function (file) { - var newbase = path.join(base,file) - if ( fs.statSync(newbase).isDirectory() ) + for (const file of files) { + var newbase = path.join(base,file) + if ( (await fs.stat(newbase)).isDirectory() ) + { + result = await recFindByExt(newbase,ext,await fs.readdir(newbase),result) + } + else + { + if ( file.substr(-1*(ext.length+1)) == '.' + ext ) { - result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result) - } - else - { - if ( file.substr(-1*(ext.length+1)) == '.' + ext ) - { - result.push(newbase) - } + result.push(newbase) } } - ) + } return result } diff --git a/src/app/app.component.html b/src/app/app.component.html index d49c48e..4d8f2a7 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -46,7 +46,7 @@ Downloads - {{subscription.name}} + {{subscription.name}} diff --git a/src/app/components/custom-playlists/custom-playlists.component.html b/src/app/components/custom-playlists/custom-playlists.component.html index 31d626f..90d2586 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.html +++ b/src/app/components/custom-playlists/custom-playlists.component.html @@ -2,7 +2,7 @@
- +
diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index 5668e32..d2d65d6 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -50,8 +50,8 @@ export class CustomPlaylistsComponent implements OnInit { }); } - goToPlaylist(event_info) { - const playlist = event_info.file; + goToPlaylist(info_obj) { + const playlist = info_obj.file; const playlistID = playlist.id; const type = playlist.type; @@ -83,7 +83,7 @@ export class CustomPlaylistsComponent implements OnInit { const playlist = args.file; const index = args.index; const playlistID = playlist.id; - this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => { + this.postsService.removePlaylist(playlistID, playlist.type).subscribe(res => { if (res['success']) { this.playlists.splice(index, 1); this.postsService.openSnackBar('Playlist successfully removed.', ''); diff --git a/src/app/components/recent-videos/recent-videos.component.html b/src/app/components/recent-videos/recent-videos.component.html index 5e99f20..258ff81 100644 --- a/src/app/components/recent-videos/recent-videos.component.html +++ b/src/app/components/recent-videos/recent-videos.component.html @@ -32,12 +32,12 @@
- +
- +
diff --git a/src/app/components/unified-file-card/unified-file-card.component.html b/src/app/components/unified-file-card/unified-file-card.component.html index e855173..a86206b 100644 --- a/src/app/components/unified-file-card/unified-file-card.component.html +++ b/src/app/components/unified-file-card/unified-file-card.component.html @@ -1,7 +1,19 @@ -
-
{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}  {{file_obj.registered | date:'shortDate'}}
+
+
{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}  {{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}
+ +
+
+ + + + + + diff --git a/src/app/components/unified-file-card/unified-file-card.component.ts b/src/app/components/unified-file-card/unified-file-card.component.ts index 7fa2082..a2a9947 100644 --- a/src/app/components/unified-file-card/unified-file-card.component.ts +++ b/src/app/components/unified-file-card/unified-file-card.component.ts @@ -1,7 +1,22 @@ -import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component'; import { DomSanitizer } from '@angular/platform-browser'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { registerLocaleData } from '@angular/common'; +import localeGB from '@angular/common/locales/en-GB'; +import localeFR from '@angular/common/locales/fr'; +import localeES from '@angular/common/locales/es'; +import localeDE from '@angular/common/locales/de'; +import localeZH from '@angular/common/locales/zh'; +import localeNB from '@angular/common/locales/nb'; + +registerLocaleData(localeGB); +registerLocaleData(localeFR); +registerLocaleData(localeES); +registerLocaleData(localeDE); +registerLocaleData(localeZH); +registerLocaleData(localeNB); @Component({ selector: 'app-unified-file-card', @@ -28,10 +43,15 @@ export class UnifiedFileCardComponent implements OnInit { @Input() use_youtubedl_archive = false; @Input() is_playlist = false; @Input() index: number; + @Input() locale = null; @Output() goToFile = new EventEmitter(); @Output() goToSubscription = new EventEmitter(); @Output() deleteFile = new EventEmitter(); @Output() editPlaylist = new EventEmitter(); + + + @ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger; + contextMenuPosition = { x: '0px', y: '0px' }; /* Planned sizes: @@ -87,6 +107,15 @@ export class UnifiedFileCardComponent implements OnInit { }); } + onRightClick(event) { + event.preventDefault(); + this.contextMenuPosition.x = event.clientX + 'px'; + this.contextMenuPosition.y = event.clientY + 'px'; + this.contextMenu.menuData = { 'item': {id: 1, name: 'hi'} }; + this.contextMenu.menu.focusFirstItem('mouse'); + this.contextMenu.openMenu(); + } + } function fancyTimeFormat(time) { diff --git a/src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html b/src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html index 98dcea9..759ae4d 100644 --- a/src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html +++ b/src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html @@ -17,7 +17,7 @@
-

NOTE: Uploading new cookies will overrride your previous cookies. Also note that cookies are instance-wide, not per-user.

+

NOTE: Uploading new cookies will override your previous cookies. Also note that cookies are instance-wide, not per-user.

diff --git a/src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html b/src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html index 0d7530b..58a7670 100644 --- a/src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html +++ b/src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html @@ -1,4 +1,4 @@ -

Editing {{sub.name}}

+

Editing

 {{sub.name}}
diff --git a/src/app/dialogs/modify-playlist/modify-playlist.component.html b/src/app/dialogs/modify-playlist/modify-playlist.component.html index f172eb6..d35db91 100644 --- a/src/app/dialogs/modify-playlist/modify-playlist.component.html +++ b/src/app/dialogs/modify-playlist/modify-playlist.component.html @@ -14,7 +14,7 @@
- +
@@ -24,5 +24,5 @@ - + \ No newline at end of file diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html index 51ec3cf..217ef6d 100644 --- a/src/app/main/main.component.html +++ b/src/app/main/main.component.html @@ -181,10 +181,12 @@ - -
-

Custom playlists

- + + +
+

Custom playlists

+ +