From 46ed0fe992f5b3cf002fc12d20e1283417f2d76e Mon Sep 17 00:00:00 2001 From: Tzahi12345 Date: Mon, 7 Sep 2020 00:39:27 -0400 Subject: [PATCH 01/26] Fixed bug in import unregistered logic where files in subfolders could not be found --- backend/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/utils.js b/backend/utils.js index efedb3b..f815c6c 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -28,7 +28,7 @@ function getDownloadedFilesByType(basePath, type) { var located_files = 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.length+1, file.length); var stats = fs.statSync(file); From 95e53b9549a76f3c9371f293dd81480713091e34 Mon Sep 17 00:00:00 2001 From: Tzahi12345 Date: Mon, 7 Sep 2020 16:06:25 -0400 Subject: [PATCH 02/26] Fixed bug where unix paths would improperly parsed while importing unregistered files --- backend/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/utils.js b/backend/utils.js index f815c6c..73156f3 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -28,7 +28,7 @@ function getDownloadedFilesByType(basePath, type) { var located_files = recFindByExt(basePath, ext); for (let i = 0; i < located_files.length; i++) { let file = located_files[i]; - var file_path = file.substring(basePath.length+1, file.length); + var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length); var stats = fs.statSync(file); From 61daf26641741fa30c4080ae6513307fe18cb173 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2020 20:55:44 +0000 Subject: [PATCH 03/26] Bump node-fetch from 2.6.0 to 2.6.1 in /backend Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1. - [Release notes](https://github.com/bitinn/node-fetch/releases) - [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1) Signed-off-by: dependabot[bot] --- backend/package-lock.json | 6 +++--- backend/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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", From d0782bb4449a16cc604ff66297ec005894e0c000 Mon Sep 17 00:00:00 2001 From: Tzahi12345 Date: Fri, 18 Sep 2020 00:46:42 -0400 Subject: [PATCH 04/26] Update README.md Updated API docs Fixes #213 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 65486db..1059d20 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. From ae8f7a2a3329c3db52afa82a051aae8ef3109176 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Fri, 18 Sep 2020 03:37:09 -0400 Subject: [PATCH 05/26] Fixed bug that prevented playlists from being navigated to --- .../components/custom-playlists/custom-playlists.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index 1f2549f..f150f8a 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -50,7 +50,8 @@ export class CustomPlaylistsComponent implements OnInit { }); } - goToPlaylist(playlist) { + goToPlaylist(info_obj) { + const playlist = info_obj.file; const playlistID = playlist.id; const type = playlist.type; From 8fdc231f08e6e1500046b617a1e603816cae0635 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Fri, 18 Sep 2020 11:22:45 -0400 Subject: [PATCH 06/26] Updated new home page UI to support file manager disabling and permissions - file manager enabled state is now cached for faster loading --- src/app/main/main.component.html | 10 ++++++---- src/app/main/main.component.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) 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

+ +
+
+
+ + + + + + 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..7d24159 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,8 @@ -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'; @Component({ selector: 'app-unified-file-card', @@ -33,6 +34,9 @@ export class UnifiedFileCardComponent implements OnInit { @Output() deleteFile = new EventEmitter(); @Output() editPlaylist = new EventEmitter(); + @ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger; + contextMenuPosition = { x: '0px', y: '0px' }; + /* Planned sizes: small: 150x175 @@ -87,6 +91,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) { From 2c43ce3c470be93909cd65bdf0e47fc0389c42ac Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Sun, 20 Sep 2020 21:30:41 -0700 Subject: [PATCH 12/26] Use async versions of filesystem methods --- backend/app.js | 849 ++++++++++++++++++++++++------------------------- 1 file changed, 411 insertions(+), 438 deletions(-) diff --git a/backend/app.js b/backend/app.js index c240dbb..f88e47b 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'); @@ -195,6 +194,16 @@ app.use(auth_api.passport.initialize()); // actual functions +/** + * setTimeout, but its a promise. + * @param {number} ms + */ +async function wait(ms) { + await new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + async function checkMigrations() { return new Promise(async resolve => { // 3.5->3.6 migration @@ -418,20 +427,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. @@ -455,87 +464,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() { @@ -544,51 +546,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); - } - - db_api.importUnregisteredFiles(); + }, subscriptionsCheckInterval * 1000); + } - // check migrations - await checkMigrations(); + db_api.importUnregisteredFiles(); - // load in previous downloads - downloads = db.get('downloads').value(); + // check migrations + await checkMigrations(); - // start the server here - startServer(); + // load in previous downloads + downloads = db.get('downloads').value(); - resolve(true); - }); + // start the server here + startServer(); + return true; } function loadConfigValues() { @@ -850,210 +846,196 @@ function getVideoFormatID(name) } async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvided = null, user_uid = null) { - return new Promise(async resolve => { - let zipFolderPath = null; + let zipFolderPath = null; - if (!fullPathProvided) { - zipFolderPath = path.join(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')); - } + 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')); + let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip')); - var archive = archiver('zip', { - gzip: true, - zlib: { level: 9 } // Sets the compression level. - }); + var archive = archiver('zip', { + gzip: true, + zlib: { level: 9 } // Sets the compression level. + }); - archive.on('error', function(err) { - logger.error(err); - throw err; - }); + archive.on('error', function(err) { + logger.error(err); + throw err; + }); - // pipe archive data to the output file - archive.pipe(output); + // 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}) - } + 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(); + await archive.finalize(); - // wait a tiny bit for the zip to reload in fs - setTimeout(function() { - resolve(path.join(zipFolderPath,outputName + '.zip')); - }, 100); + // 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) { + 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'); -} + jsonPath = path.join(__dirname, jsonPath); + altJSONPath = path.join(__dirname, altJSONPath); + audioFilePath = path.join(__dirname, audioFilePath); -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'); - - jsonPath = path.join(__dirname, jsonPath); - altJSONPath = path.join(__dirname, altJSONPath); - audioFilePath = path.join(__dirname, audioFilePath); - - let jsonExists = fs.existsSync(jsonPath); - let thumbnailExists = fs.existsSync(thumbnailPath); - - if (!jsonExists) { - if (fs.existsSync(altJSONPath)) { - jsonExists = true; - jsonPath = altJSONPath; - } - } + let jsonExists = await fs.pathExists(jsonPath); + let thumbnailExists = await fs.pathExists(thumbnailPath); - if (!thumbnailExists) { - if (fs.existsSync(altThumbnailPath)) { - thumbnailExists = true; - thumbnailPath = altThumbnailPath; - } + if (!jsonExists) { + if (await fs.pathExists(altJSONPath)) { + jsonExists = true; + jsonPath = altJSONPath; } + } - let audioFileExists = fs.existsSync(audioFilePath); + 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) { + 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 = 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')); - } + // 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 ? subscriptions_api.removeIDFromArchive(archive_path, id) : null; + if (blacklistMode && line) await writeToBlacklist('audio', line); + } else { + logger.info('Could not find archive file for audio files. Creating...'); + await fs.close(await fs.open(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); - } - }); + 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 { - // TODO: tell user that the file didn't exist - resolve(true); + 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 (!thumbnailExists) { - if (fs.existsSync(altThumbnailPath)) { - thumbnailExists = true; - thumbnailPath = altThumbnailPath; - } + if (!jsonExists) { + if (await fs.pathExists(altJSONPath)) { + jsonExists = true; + jsonPath = altJSONPath; } + } - 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 (!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) { + } + } - 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 = 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')); - } + // 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 ? subscriptions_api.removeIDFromArchive(archive_path, id) : null; + if (blacklistMode && line) await 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); - } - }); + 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 { - // TODO: tell user that the file didn't exist - resolve(true); + return true; } - - }); + } else { + // TODO: tell user that the file didn't exist + return true; + } } // replaces .webm with appropriate extension @@ -1072,38 +1054,38 @@ function getTrueFileName(unfixed_path, type) { 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); - try { - result.push(JSON.parse(data)); - } catch(e) { - logger.error(`Could not find info for file ${fileName}.mp3`); - } +/** + * @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'; } - } - return result; -} -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); + 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}.mp4`); + 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; + })); + + return result.filter(data => data != null); } // downloads @@ -1401,133 +1383,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) { - downloadConfig = ['-o', path.join(fileFolderPath, 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']; - } + 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 && options.downloading_method === 'exec') downloadConfig.push(...qualityPath); - if (is_audio && !options.skip_audio_args) { - downloadConfig.push('-x'); - downloadConfig.push('--audio-format', 'mp3'); - } + 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 (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 (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); - } + 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`); + 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); + await fs.ensureDir(archive_folder); - // create archive file if it doesn't exist - if (!fs.existsSync(archive_path)) { - fs.closeSync(fs.openSync(archive_path, 'w')); - } + // 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 (!fs.existsSync(blacklist_path)) { - fs.closeSync(fs.openSync(blacklist_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`); - fs.ensureFileSync(merged_path); - // merges blacklist and regular archive - let inputPathList = [archive_path, blacklist_path]; - let status = await mergeFiles(inputPathList, merged_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 = fs.readFileSync(merged_path, "utf8"); + options.merged_string = await fs.readFile(merged_path, "utf8"); - downloadConfig.push('--download-archive', merged_path); - } + downloadConfig.push('--download-archive', merged_path); + } - if (config_api.getConfigItem('ytdl_include_thumbnail')) { - downloadConfig.push('--write-thumbnail'); - } + 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(',,')); + 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(',')}`); - resolve(downloadConfig); - }); + + } + 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); @@ -1593,11 +1573,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 @@ -1741,21 +1721,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()); @@ -1771,8 +1736,12 @@ app.use(function(req, res, next) { next(); } else if (req.query.apiKey === admin_token) { next(); - } else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) { - next(); + } else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key')) { + if (req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) { + next(); + } else { + res.status(401).send('Invalid API key'); + } } else if (req.path.includes('/api/video/') || req.path.includes('/api/audio/')) { next(); } else { @@ -1878,7 +1847,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'); @@ -1900,8 +1869,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(); @@ -1915,10 +1897,7 @@ app.get('/api/getMp3s', optionalJwt, function(req, res) { mp3s = JSON.parse(JSON.stringify(mp3s)); // 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, @@ -1927,7 +1906,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(); @@ -1942,10 +1921,7 @@ app.get('/api/getMp4s', optionalJwt, function(req, res) { mp4s = JSON.parse(JSON.stringify(mp4s)); // 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({ mp4s: mp4s, @@ -2018,7 +1994,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]; @@ -2034,10 +2010,7 @@ app.post('/api/getAllFiles', optionalJwt, function (req, res) { files = JSON.parse(JSON.stringify(files)); // 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, @@ -2465,7 +2438,7 @@ app.post('/api/deleteMp3', optionalJwt, async (req, res) => { var name = audio_obj.id; var fullpath = audioFolderPath + name + ".mp3"; var wasDeleted = false; - if (fs.existsSync(fullpath)) + if (await fs.pathExists(fullpath)) { deleteAudioFile(name, null, blacklistMode); db.get('files.audio').remove({uid: uid}).write(); @@ -2496,7 +2469,7 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => { var name = video_obj.id; var fullpath = videoFolderPath + name + ".mp4"; var wasDeleted = false; - if (fs.existsSync(fullpath)) + if (await fs.pathExists(fullpath)) { wasDeleted = await deleteVideoFile(name, null, blacklistMode); db.get('files.video').remove({uid: uid}).write(); @@ -2582,7 +2555,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); @@ -2594,14 +2567,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); @@ -2824,9 +2797,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}`) @@ -2842,8 +2815,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); @@ -2860,10 +2835,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); @@ -2936,7 +2909,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}); From f535d18cb900b3fcc211050b858ae31119acde83 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Sat, 26 Sep 2020 15:14:37 -0700 Subject: [PATCH 13/26] Use async methods in auth and subscriptions --- backend/app.js | 8 +- backend/authentication/auth.js | 50 +++-- backend/subscriptions.js | 332 ++++++++++++++++----------------- 3 files changed, 185 insertions(+), 205 deletions(-) diff --git a/backend/app.js b/backend/app.js index f88e47b..7ffdb7a 100644 --- a/backend/app.js +++ b/backend/app.js @@ -940,7 +940,7 @@ async function deleteAudioFile(name, customPath = null, blacklistMode = false) { // 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 ? subscriptions_api.removeIDFromArchive(archive_path, id) : null; + const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null; if (blacklistMode && line) await writeToBlacklist('audio', line); } else { logger.info('Could not find archive file for audio files. Creating...'); @@ -1015,7 +1015,7 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) { // 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 ? subscriptions_api.removeIDFromArchive(archive_path, id) : null; + const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null; if (blacklistMode && line) await writeToBlacklist('video', line); } else { logger.info('Could not find archive file for videos. Creating...'); @@ -2429,7 +2429,7 @@ app.post('/api/deleteMp3', optionalJwt, async (req, res) => { var blacklistMode = req.body.blacklistMode; if (req.isAuthenticated()) { - let success = auth_api.deleteUserFile(req.user.uid, uid, 'audio', blacklistMode); + let success = await auth_api.deleteUserFile(req.user.uid, uid, 'audio', blacklistMode); res.send(success); return; } @@ -2460,7 +2460,7 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => { var blacklistMode = req.body.blacklistMode; if (req.isAuthenticated()) { - let success = auth_api.deleteUserFile(req.user.uid, uid, 'video', blacklistMode); + let success = await auth_api.deleteUserFile(req.user.uid, uid, 'video', blacklistMode); res.send(success); return; } diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index 173dd37..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 @@ -352,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) { @@ -375,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 @@ -397,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); } } } @@ -533,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/subscriptions.js b/backend/subscriptions.js index a17dc1d..104568d 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); - - if (jsonExists) { - retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id']; - fs.unlinkSync(jsonPath); - } - if (imageFileExists) { - fs.unlinkSync(imageFilePath); - } + 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'); + + const [jsonExists, videoFileExists, imageFileExists, altImageFileExists] = await Promise.all([ + fs.pathExists(jsonPath), + fs.pathExists(videoFilePath), + fs.pathExists(imageFilePath), + fs.pathExists(altImageFilePath), + ]); + + 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 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'); + // 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'); + const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); - let appendedBasePath = null - appendedBasePath = getAppendedBasePath(sub, basePath); + let appendedBasePath = null + appendedBasePath = getAppendedBasePath(sub, basePath); - let multiUserMode = null; - if (user_uid) { - multiUserMode = { - user: user_uid, - file_path: appendedBasePath - } + let multiUserMode = null; + if (user_uid) { + multiUserMode = { + user: user_uid, + file_path: appendedBasePath } + } - const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4' + const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4' - let fullOutput = `${appendedBasePath}/%(title)s.%(ext)s`; - if (sub.custom_output) { - fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`; - } + let fullOutput = `${appendedBasePath}/%(title)s.%(ext)s`; + if (sub.custom_output) { + fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`; + } - let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json']; + let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json']; - 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 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'] + } - downloadConfig.push(...qualityPath) + 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); + 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); + } - let archive_dir = null; - let archive_path = null; + 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 (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 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); - } + 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.'); - } + 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.'); } + } - if (config_api.getConfigItem('ytdl_include_thumbnail')) { - downloadConfig.push('--write-thumbnail'); - } + if (config_api.getConfigItem('ytdl_include_thumbnail')) { + downloadConfig.push('--write-thumbnail'); + } + + // get videos + logger.verbose('Subscription: getting videos for subscription ' + sub.name); - // 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) { @@ -456,23 +453,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; @@ -493,7 +475,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; } From 21eafeab22ee654f987c80061b04074d15537605 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Sat, 26 Sep 2020 15:22:57 -0700 Subject: [PATCH 14/26] Make utils.recFindByExt and utils.getDownloadedFilesByType async --- backend/app.js | 111 +++++++++++++++++++---------------------------- backend/db.js | 2 +- backend/utils.js | 17 ++++---- 3 files changed, 55 insertions(+), 75 deletions(-) diff --git a/backend/app.js b/backend/app.js index 7ffdb7a..0f2090e 100644 --- a/backend/app.js +++ b/backend/app.js @@ -205,54 +205,49 @@ async function wait(ms) { } 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+'); } - }); - } + // 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(); - resolve(true); - }); + 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(); - - 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'); - } + 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}`); + 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'); - } + 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(); - resolve(true); - } catch(err) { - logger.error(err); - resolve(false); } - }); + + // 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() { @@ -701,17 +696,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; @@ -730,9 +725,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); @@ -740,7 +735,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; @@ -934,7 +929,7 @@ async function deleteAudioFile(name, customPath = null, blacklistMode = false) { // get ID from JSON - var jsonobj = utils.getJSONMp3(name, filePath); + var jsonobj = await utils.getJSONMp3(name, filePath); let id = null; if (jsonobj) id = jsonobj.id; @@ -1009,7 +1004,7 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) { // get ID from JSON - var jsonobj = utils.getJSONMp4(name, filePath); + var jsonobj = await utils.getJSONMp4(name, filePath); let id = null; if (jsonobj) id = jsonobj.id; @@ -1038,22 +1033,6 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) { } } -// 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; -} - /** * @param {'audio' | 'video'} type * @param {string[]} fileNames @@ -2214,7 +2193,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); diff --git a/backend/db.js b/backend/db.js index c46cfd8..81524aa 100644 --- a/backend/db.js +++ b/backend/db.js @@ -184,7 +184,7 @@ 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 => { // 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 diff --git a/backend/utils.js b/backend/utils.js index 73156f3..d7ac82d 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 = 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,7 +159,7 @@ function deleteJSONFile(name, type, customPath = null) { } -function recFindByExt(base,ext,files,result) +async function recFindByExt(base,ext,files,result) { files = files || fs.readdirSync(base) result = result || [] @@ -168,7 +169,7 @@ function recFindByExt(base,ext,files,result) var newbase = path.join(base,file) if ( fs.statSync(newbase).isDirectory() ) { - result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result) + result = await recFindByExt(newbase,ext,fs.readdirSync(newbase),result) } else { From cdd2f78998e6445347da26ffe089afc881d3fea1 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 27 Sep 2020 05:05:45 -0400 Subject: [PATCH 15/26] Fixed bug that prevented video playlists from being deleted --- .../components/custom-playlists/custom-playlists.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index f150f8a..d2d65d6 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -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.', ''); From e26ac82c6634aca0cc06f86f69370104b6697634 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Tue, 29 Sep 2020 08:53:36 -0700 Subject: [PATCH 16/26] Fix missing keywords --- backend/app.js | 2 +- backend/db.js | 4 ++-- backend/utils.js | 26 ++++++++++++-------------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/backend/app.js b/backend/app.js index 0f2090e..d58e350 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1946,7 +1946,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 = []; diff --git a/backend/db.js b/backend/db.js index 81524aa..6f12be1 100644 --- a/backend/db.js +++ b/backend/db.js @@ -182,7 +182,7 @@ 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 = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type); @@ -195,7 +195,7 @@ async function importUnregisteredFiles() { logger.verbose(`Added discovered file to the database: ${file.id}`); } }); - }); + } } diff --git a/backend/utils.js b/backend/utils.js index d7ac82d..4bfad5e 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -161,25 +161,23 @@ function deleteJSONFile(name, type, customPath = null) { 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() ) - { - result = await recFindByExt(newbase,ext,fs.readdirSync(newbase),result) - } - else + for (const file in 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 ) { - if ( file.substr(-1*(ext.length+1)) == '.' + ext ) - { - result.push(newbase) - } + result.push(newbase) } } - ) + } return result } From 70ee071e570cd0545dcac584a6a57b408e9242ee Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 29 Sep 2020 14:41:38 -0400 Subject: [PATCH 17/26] Cleaned up spanish translation file --- src/assets/i18n/messages.es.xlf | 592 -------------------------------- 1 file changed, 592 deletions(-) diff --git a/src/assets/i18n/messages.es.xlf b/src/assets/i18n/messages.es.xlf index b224dc0..6f0d89e 100644 --- a/src/assets/i18n/messages.es.xlf +++ b/src/assets/i18n/messages.es.xlf @@ -5,1386 +5,794 @@ No need to include URL, just everything after. No es necesario incluir URL, solo todo después - string - rsUnused Global custom args for downloads on the home page. Argumentos personalizados globales para descargas en la página de inicio. - string - rsUnused Create a playlist Crea una lista de reproducción - Create a playlist dialog title - string - rsInUse Name Nombre - Playlist name placeholder - string - rsInUse Audio files Archivos de sonido - Audio files title - string - rsInUse Videos Archivos de video - Videos title - string - rsInUse Modify youtube-dl args Modificar args de youtube-dl - Modify args title - string - rsInUse Simulated new args Args nuevos simulados - Simulated args title - string - rsInUse Add an arg Añadir un arg - Add arg card title - string - rsInUse Search by category Busqueda por categoria - Search args by category button - string - rsInUse Use arg value Usar valor de arg - Use arg value checkbox - string - rsInUse Arg value Valor de arg - Arg value placeholder - string - rsInUse Add arg Añadir arg - Search args by category button - string - rsInUse Cancel Cancelar - Arg modifier cancel button - string - rsInUse Modify Modificar - Arg modifier modify button - string - rsInUse Youtube Downloader Descargador de Youtube - Youtube downloader home page label - string - rsInUse Please enter a valid URL! Por favor entre una URL válida - Enter valid URL error - string - rsInUse Quality Calidad - Quality select label - string - rsInUse Use URL Usa URL - YT search Use URL button for searched video - string - rsInUse View Ver - YT search View button for searched video - string - rsInUse Only Audio Solo audio - Only Audio checkbox - string - rsInUse Multi-download Mode Descarga múltiple - Multi-download Mode checkbox - string - rsInUse Download Descarga - Main download button - string - rsInUse Cancel Cancelar - Cancel download button - string - rsInUse Advanced Avanzado - Advanced download mode panel - string - rsInUse Simulated command: Commando simulado: - Simulated command label - string - rsInUse Use custom args Usar argumentos personalizados - Use custom args checkbox - string - rsInUse Custom args Argumentos personalizados - Custom args placeholder - string - rsInUse No need to include URL, just everything after. Args are delimited using two commas like so: ,, No es necesario incluir URL, solo todo después. Los argumentos se delimitan usando dos comas así: ,, - Custom Args input hint - string - rsNew Use custom output Usar salida personalizada - Use custom output checkbox - string - rsInUse Custom output Salida personalizada - Custom output placeholder - string - rsInUse Documentation Documentación - Youtube-dl output template documentation link - string - rsInUse Path is relative to the config download path. Don't include extension. La ruta es relativa a la ruta de descarga de la config. No incluya el extensión. - Custom Output input hint - string - rsInUse Use authentication Usa autenticación - Use authentication checkbox - string - rsInUse Username Nombre de usuario - YT Username placeholder - string - rsInUse Password Contraseña - YT Password placeholder - string - rsInUse Audio Audio - Audio files title - string - rsInUse Your audio files are here Tus archivos de audio están aquí - Audio files description - string - rsInUse Playlists Listas de reproducción - Playlists title - string - rsInUse No playlists available. Create one from your downloading audio files by clicking the blue plus button. No hay listas de reproducción disponibles. Cree uno de tus archivos de audio haciendo clic en el botón azul más. - No video playlists available text - string - rsInUse Video Vídeo - Video files title - string - rsInUse Your video files are here Tus archivos de video son aquí - Video files description - string - rsInUse No playlists available. Create one from your downloading video files by clicking the blue plus button. No hay listas de reproducción disponibles. Cree uno de tus archivos de video haciendo clic en el botón azul más. - No video playlists available text - string - rsInUse Name: Nombre: - Video name property - string - rsInUse URL: URL: - Video URL property - string - rsInUse Uploader: Cargador: - Video ID property - string - rsInUse File size: Tamaño del archivo: - Video file size property - string - rsInUse Path: Ruta: - Video path property - string - rsInUse Upload Date: Subido: - Video upload date property - string - rsInUse Close Cerca - Close subscription info button - string - rsInUse ID: ID: - File or playlist ID - string - rsInUse Count: Cuenta: - Playlist video count - string - rsInUse Info Información - Video info button - string - rsInUse Delete Eliminar - Delete video button - string - rsInUse Delete and blacklist Eliminar y pones en la lista negra - Delete and blacklist video button - string - rsInUse Settings Configuraciones - Settings title - string - rsInUse URL URL - URL input placeholder - string - rsInUse URL this app will be accessed from, without the port. URL desde la que se accederá a esta aplicación, sin el puerto. - URL setting input hint - string - rsInUse Port Puerto - Port input placeholder - string - rsInUse The desired port. Default is 17442. Puerto deseado. El valor predeterminado es 17442. - Port setting input hint - string - rsInUse Multi-user mode Modo multiusuario - Multi user mode setting - string - rsNew Users base path Ruta base de usuarios - Users base path placeholder - string - rsNew Base path for users and their downloaded videos. Ruta base para los usuarios y sus videos descargados. - Users base path hint - string - rsNew Use encryption Usa cifrado - Use encryption setting - string - rsInUse Cert file path Ruta del archivo de certificado - Cert file path input placeholder - string - rsInUse Key file path Ruta de archivo de clave - Key file path input placeholder - string - rsInUse Allow subscriptions Permitir suscripciones - Allow subscriptions setting - string - rsInUse Subscriptions base path Ruta base de suscripciones - Subscriptions base path input setting placeholder - string - rsInUse Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder. Ruta base para videos de sus canales y listas de reproducción suscritos. Es relativo a la carpeta raíz de YTDL-Material. - Subscriptions base path setting input hint - string - rsInUse Check interval Intervalo de comprobación - Check interval input setting placeholder - string - rsInUse Unit is seconds, only include numbers. La unidad es segundos, solo incluye números. - Check interval setting input hint - string - rsInUse Use youtube-dl archive Usa el archivo de youtube-dl - Use youtube-dl archive setting - string - rsInUse With youtube-dl's archive Con la función de archivo de youtube-dl, - youtube-dl archive explanation prefix link - string - rsInUse feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory. los videos descargados de sus suscripciones se graban en un archivo de texto en el subdirectorio del archivo de suscripciones. - youtube-dl archive explanation middle - string - rsInUse This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss. Esto permite eliminar videos de sus suscripciones de forma permanente sin darse de baja y le permite grabar los videos que descargó en caso de pérdida de datos. - youtube-dl archive explanation suffix - string - rsInUse Theme Tema - Theme select label - string - rsInUse Default Defecto - Default theme label - string - rsInUse Dark Oscura - Dark theme label - string - rsInUse Allow theme change Permitir cambio de tema - Allow theme change setting - string - rsInUse Language Idioma - Language select label - string - rsInUse Main Principal - Main settings label - string - rsInUse Audio folder path Ruta de la carpeta de audio - Audio folder path input placeholder - string - rsInUse Path for audio only downloads. It is relative to YTDL-Material's root folder. Ruta para descargas de solo audio. Es relativo a la carpeta raíz de YTDL-Material. - Aduio path setting input hint - string - rsInUse Video folder path Ruta de la carpeta de video - Video folder path input placeholder - string - rsInUse Path for video downloads. It is relative to YTDL-Material's root folder. Ruta de descarga de videos. Es relativo a la carpeta raíz de YTDL-Material. - Video path setting input hint - string - rsInUse Global custom args for downloads on the home page. Args are delimited using two commas like so: ,, Argumentos personalizados globales para descargas en la página de inicio. Los argumentos se delimitan usando dos comas así: ,, - Custom args setting input hint - string - rsNew Downloader Descargador - Downloader settings label - string - rsInUse Top title Título superior - Top title input placeholder - string - rsInUse File manager enabled Administrador de archivos habilitado - File manager enabled setting - string - rsInUse Downloads manager enabled Administrador de descargas habilitado - Downloads manager enabled setting - string - rsNew Allow quality select Permitir selección de calidad - Allow quality seelct setting - string - rsInUse Download only mode Modo de solo descarga - Download only mode setting - string - rsInUse Allow multi-download mode Permitir el modo de descarga múltiple - Allow multi-download mode setting - string - rsInUse Require pin for settings Requiere pin para la configuración - Require pin for settings setting - string - rsInUse Set New Pin Establecer nuevo pin - Set new pin button - string - rsInUse Enable Public API Habilitar API pública - Enable Public API key setting - string - rsInUse Public API Key Clave API pública - Public API Key setting placeholder - string - rsInUse View documentation Ver documentación - View API docs setting hint - string - rsInUse Generate Generar - Generate key button - string - rsInUse Use YouTube API Utilizar la API de YouTube - Use YouTube API setting - string - rsInUse Youtube API Key Clave API de YouTube - Youtube API Key setting placeholder - string - rsInUse Generating a key is easy! ¡Generar una clave es fácil! - Youtube API Key setting hint - string - rsInUse Click here ¡Haga clic aquí - Chrome ext click here - string - rsInUse to download the official YoutubeDL-Material Chrome extension manually. para descargar la extensión Chrome oficial de YoutubeDL-Material manualmente. - Chrome click here suffix - string - rsInUse You must manually load the extension and modify the extension's settings to set the frontend URL. Debe cargar manualmente la extensión y modificar la configuración de la extensión para establecer la URL de la interfaz. - Chrome setup suffix - string - rsInUse to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page. para instalar la extensión Firefox oficial de YoutubeDL-Material directamente desde la página de extensiones de Firefox. - Firefox click here suffix - string - rsInUse Detailed setup instructions. Instrucciones detalladas de configuración. - Firefox setup prefix link - string - rsInUse Not much is required other than changing the extension's settings to set the frontend URL. No se requiere mucho más que cambiar la configuración de la extensión para establecer la URL de la interfaz. - Firefox setup suffix - string - rsInUse Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark. Arrastra el enlace de abajo a tus marcadores, ¡y listo! Simplemente navegue hasta el video de YouTube que desea descargar y haga clic en el marcador. - Bookmarklet instructions - string - rsInUse Generate 'audio only' bookmarklet Generar bookmarklet solo de audio - Generate audio only bookmarklet checkbox - string - rsInUse Extra Extra - Extra settings label - string - rsInUse Use default downloading agent Usar agente de descarga predeterminado - Use default downloading agent setting - string - rsInUse Select a downloader Seleccione un descargador - Custom downloader select label - string - rsInUse Allow advanced download Permitir descarga avanzada - Allow advanced downloading setting - string - rsInUse Advanced Avanzado - Host settings label - string - rsInUse Allow user registration Permitir registro de usuario - Allow registration setting - string - rsNew Users Usuarios - Users settings label - string - rsNew Save Salvar - Settings save button - string - rsInUse {VAR_SELECT, select, true {Close} false {Cancel} other {otha} } {VAR_SELECT, select, true {Cerrar} false {Cancelar} other {Otro} } - Settings cancel and close button - string - rsInUse About YoutubeDL-Material Sobre YoutubeDL-Material - About dialog title - string - rsInUse is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos. es un descargador de código abierto de YouTube creado bajo las especificaciones de "Material Design" de Google. Puede descargar sin problemas sus videos favoritos como archivos de video o audio, e incluso suscribirse a sus canales favoritos y listas de reproducción para mantenerse actualizado con sus nuevos videos. - About first paragraph - string - rsInUse has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above. tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones compatibles haciendo clic en el icono de GitHub que se encuentra arriba. - About second paragraph - string - rsInUse Installed version: Versión instalada: - Version label - string - rsInUse Checking for updates... Comprobando actualizaciones... - Checking for updates text - string - rsInUse Update available Actualización disponible - View latest update - string - rsInUse You can update from the settings menu. Puede actualizar desde el menú de configuración. - Update through settings menu hint - string - rsInUse Found a bug or have a suggestion? ¿Encontró un error o tiene una sugerencia? - About bug prefix - string - rsInUse to create an issue! para crear una cuestión! - About bug suffix - string - rsInUse Your Profile Tu perfil - User profile dialog title - string - rsNew UID: UID: - UID - string - rsNew Created: Creado: - Created - string - rsNew You are not logged in. Usted no se ha identificado. - Not logged in notification - string - rsNew Login Identificarse - Login - string - rsNew Logout Salir - Logout - string - rsNew Create admin account Crear cuenta de administrador - Create admin account dialog title - string - rsNew No default admin account detected. This will create and set the password for an admin account with the user name as 'admin'. No se detectó una cuenta de administrador predeterminada. Esto creará y establecerá la contraseña para una cuenta de administrador con el nombre de usuario como 'admin'. - No default admin detected explanation - string - rsNew Create Crear - Create - string - rsNew Profile Perfil - Profile menu label - string - rsNew About Sobre - About menu label - string - rsInUse Home Inicio - Navigation menu Home Page title - string - rsInUse Subscriptions Suscripciones - Navigation menu Subscriptions Page title - string - rsInUse Downloads Descargas - Navigation menu Downloads Page title - string - rsNew Share playlist Compartir lista de reproducción - Share playlist dialog title - string - rsInUse Share video Compartir vídeo - Share video dialog title - string - rsInUse Share audio Compartir audio - Share audio dialog title - string - rsInUse Enable sharing Habilitar compartir - Enable sharing checkbox - string - rsInUse Use timestamp Usar marca de tiempo - Use timestamp - string - rsNew Seconds Segundos - Seconds - string - rsNew Copy to clipboard Copiar al Portapapeles - Copy to clipboard button - string - rsInUse Save changes Guardar cambios - Playlist save changes button - string - rsInUse Details Detalles - Details - string - rsNew An error has occured: Se ha producido un error: - Error label - string - rsNew Download start: Inicio de descarga: - Download start label - string - rsNew Download end: Fin de descarga: - Download end label - string - rsNew File path(s): Ruta(s) del archivo: - File path(s) label - string - rsNew Subscribe to playlist or channel Suscríbase a la lista de reproducción o al canal - Subscribe dialog title - string - rsInUse The playlist or channel URL La lista de reproducción o la URL del canal - Subscription URL input hint - string - rsInUse Custom name Nombre personalizado - Subscription custom name placeholder - string - rsInUse This is optional Esto es opcional - Custom name input hint - string - rsInUse Download all uploads Descargar todas las cargas - Download all uploads subscription setting - string - rsInUse Download videos uploaded in the last Descargar videos subidos en el último - Download time range prefix - string - rsInUse Streaming-only mode Modo de solo transmisión - Streaming-only mode - string - rsNew Subscribe Subscribe - Subscribe button - string - rsInUse Type: Tipo: - Subscription type property - string - rsInUse Archive: Archivo: - Subscription ID property - string - rsInUse Export Archive Exportar el archivo - Export Archive button - string - rsInUse Unsubscribe Darse de baja - Unsubscribe button - string - rsInUse Your subscriptions Sus suscripciones - Subscriptions title - string - rsInUse Channels Canales - Subscriptions channels title - string - rsInUse Name not available. Channel retrieval in progress. Nombre no disponible. Recuperación de canales en progreso. - Subscription playlist not available text - string - rsInUse You have no channel subscriptions. No tienes suscripciones de canal. - No channel subscriptions text - string - rsInUse Name not available. Playlist retrieval in progress. Nombre no disponible. Recuperación de listas de reproducción en progreso. - Subscription playlist not available text - string - rsInUse You have no playlist subscriptions. No tienes suscripciones a listas de reproducción. - No playlist subscriptions text - string - rsInUse Search Buscar - Subscription videos search placeholder - string - rsInUse Length: Duración: - Video duration label - string - rsInUse Delete and redownload Eliminar y volver a descargar - Delete and redownload subscription video button - string - rsInUse Delete forever Borrar para siempre - Delete forever subscription video button - string - rsInUse Updater Updater - Update progress dialog title - string - rsInUse Select a version: Seleccione una versión: - Select a version - string - rsInUse Register Registrarse - Register - string - rsNew Session ID: ID de sesión: - Session ID - string - rsNew (current) (actual) - Current session - string - rsNew No downloads available! ¡No hay descargas disponibles! - No downloads label - string - rsNew Register a user Registrar un usuario - Register user dialog title - string - rsNew User name Nombre de usuario - User name placeholder - string - rsNew Manage user Administrar usuario - Manage user dialog title - string - rsNew User UID: UID de usuario: - User UID - string - rsNew New password Nueva contraseña - New password placeholder - string - rsNew Set new password Establecer nueva contraseña - Set new password - string - rsNew Use default Uso por defecto - Use default - string - rsNew Yes Si - Yes - string - rsNew No No - No - string - rsNew Manage role Gestionar rol - Manage role dialog title - string - rsNew User name Nombre de usuario - Username users table header - string - rsNew Role Rol - Role users table header - string - rsNew Actions Acciones - Actions users table header - string - rsNew Add Users Agregar Usuarios - Add users button - string - rsNew Edit Role Editar Rol - Edit role - string - rsNew From b8280e86463d193590a58b88f1a545c53621ef2f Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 29 Sep 2020 16:58:17 -0400 Subject: [PATCH 18/26] Updated spanish translation file (2) --- src/assets/i18n/messages.es.xlf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/assets/i18n/messages.es.xlf b/src/assets/i18n/messages.es.xlf index 6f0d89e..f116163 100644 --- a/src/assets/i18n/messages.es.xlf +++ b/src/assets/i18n/messages.es.xlf @@ -1,6 +1,6 @@ - - - + + + No need to include URL, just everything after. From 3e4e7edd900c483f2af62e150629704bde730718 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Tue, 29 Sep 2020 14:32:28 -0700 Subject: [PATCH 19/26] Oops. for in -> for of --- backend/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/utils.js b/backend/utils.js index 4bfad5e..411a7da 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -164,7 +164,7 @@ async function recFindByExt(base,ext,files,result) files = files || (await fs.readdir(base)) result = result || [] - for (const file in files) { + for (const file of files) { var newbase = path.join(base,file) if ( (await fs.stat(newbase)).isDirectory() ) { From 91b892b21a3f960340a0f26ee63b3ce207f60c4d Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 30 Sep 2020 04:19:04 -0400 Subject: [PATCH 20/26] Updated source translation file --- src/assets/i18n/messages.en.xlf | 680 ++++++++++++++++++++------------ 1 file changed, 418 insertions(+), 262 deletions(-) diff --git a/src/assets/i18n/messages.en.xlf b/src/assets/i18n/messages.en.xlf index 2df52e6..6d2ddf9 100644 --- a/src/assets/i18n/messages.en.xlf +++ b/src/assets/i18n/messages.en.xlf @@ -14,7 +14,7 @@ Name app/create-playlist/create-playlist.component.html - 5 + 6 app/dialogs/modify-playlist/modify-playlist.component.html @@ -22,11 +22,35 @@ Playlist name placeholder + + Audio + + app/create-playlist/create-playlist.component.html + 12 + + Audio + + + Video + + app/create-playlist/create-playlist.component.html + 13 + + Video + + + Type + + app/create-playlist/create-playlist.component.html + 11 + + Type select + Audio files app/create-playlist/create-playlist.component.html - 10 + 19 Audio files title @@ -34,7 +58,7 @@ Videos app/create-playlist/create-playlist.component.html - 11 + 20 app/subscription/subscription/subscription.component.html @@ -106,7 +130,15 @@ app/dialogs/subscribe-dialog/subscribe-dialog.component.html - 70 + 72 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 56 + + + app/components/modify-users/modify-users.component.html + 61 Arg modifier cancel button @@ -118,21 +150,13 @@ Arg modifier modify button - - Youtube Downloader - - app/main/main.component.html - 5 - - Youtube downloader home page label - Quality app/main/main.component.html - 21 + 18 Quality select label @@ -140,7 +164,7 @@ Use URL app/main/main.component.html - 49 + 46 YT search Use URL button for searched video @@ -150,7 +174,7 @@ app/main/main.component.html - 52 + 49 YT search View button for searched video @@ -160,7 +184,7 @@ app/main/main.component.html - 62 + 59 Only Audio checkbox @@ -170,7 +194,7 @@ app/main/main.component.html - 67 + 64 Multi-download Mode checkbox @@ -180,7 +204,7 @@ app/main/main.component.html - 76 + 73 Main download button @@ -190,7 +214,7 @@ app/main/main.component.html - 81 + 78 Cancel download button @@ -200,7 +224,7 @@ app/main/main.component.html - 93 + 90 Advanced download mode panel @@ -210,7 +234,7 @@ app/main/main.component.html - 99 + 96 Simulated command label @@ -220,7 +244,7 @@ app/main/main.component.html - 107 + 104 Use custom args checkbox @@ -228,15 +252,19 @@ Custom args app/main/main.component.html - 113 + 110 app/settings/settings.component.html - 145 + 120 app/dialogs/subscribe-dialog/subscribe-dialog.component.html - 48 + 50 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 34 Custom args placeholder @@ -246,7 +274,7 @@ app/main/main.component.html - 115 + 112 Custom Args input hint @@ -256,7 +284,7 @@ app/main/main.component.html - 123 + 120 Use custom output checkbox @@ -264,7 +292,7 @@ Custom output app/main/main.component.html - 128 + 125 Custom output placeholder @@ -272,11 +300,15 @@ Documentation app/main/main.component.html - 130 + 127 app/dialogs/subscribe-dialog/subscribe-dialog.component.html - 60 + 62 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 46 Youtube-dl output template documentation link @@ -284,11 +316,15 @@ Path is relative to the config download path. Don't include extension. app/main/main.component.html - 131 + 128 app/dialogs/subscribe-dialog/subscribe-dialog.component.html - 61 + 63 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 47 Custom Output input hint @@ -298,7 +334,7 @@ app/main/main.component.html - 137 + 134 Use authentication checkbox @@ -306,7 +342,7 @@ Username app/main/main.component.html - 142 + 139 YT Username placeholder @@ -314,7 +350,7 @@ Password app/main/main.component.html - 147 + 144 app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html @@ -326,82 +362,6 @@ YT Password placeholder - - - Audio - - - app/main/main.component.html - 191 - - Audio files title - - - - Your audio files are here - - - app/main/main.component.html - 196 - - Audio files description - - - Playlists - - app/main/main.component.html - 211 - - - app/main/main.component.html - 253 - - - app/subscriptions/subscriptions.component.html - 27 - - Playlists title - - - - No playlists available. Create one from your downloading audio files by clicking the blue plus button. - - - app/main/main.component.html - 222 - - No video playlists available text - - - - Video - - - app/main/main.component.html - 232 - - Video files title - - - - Your video files are here - - - app/main/main.component.html - 237 - - Video files description - - - - No playlists available. Create one from your downloading video files by clicking the blue plus button. - - - app/main/main.component.html - 266 - - No video playlists available text - Name: @@ -468,6 +428,10 @@ app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html 40 + + app/dialogs/about-dialog/about-dialog.component.html + 59 + app/dialogs/user-profile-dialog/user-profile-dialog.component.html 27 @@ -536,6 +500,10 @@ app/file-card/file-card.component.html 19 + + app/components/unified-file-card/unified-file-card.component.html + 32 + Playlist edit button @@ -548,6 +516,14 @@ app/file-card/file-card.component.html 25 + + app/components/unified-file-card/unified-file-card.component.html + 28 + + + app/components/unified-file-card/unified-file-card.component.html + 34 + Delete playlist @@ -560,6 +536,10 @@ app/subscription/subscription-file-card/subscription-file-card.component.html 7 + + app/components/unified-file-card/unified-file-card.component.html + 19 + Video info button @@ -568,6 +548,10 @@ app/file-card/file-card.component.html 26 + + app/components/unified-file-card/unified-file-card.component.html + 29 + Delete and blacklist video button @@ -666,35 +650,11 @@ Users base path hint - - Use encryption - - app/settings/settings.component.html - 48 - - Use encryption setting - - - Cert file path - - app/settings/settings.component.html - 53 - - Cert file path input placeholder - - - Key file path - - app/settings/settings.component.html - 59 - - Key file path input placeholder - Allow subscriptions app/settings/settings.component.html - 68 + 48 Allow subscriptions setting @@ -702,7 +662,7 @@ Subscriptions base path app/settings/settings.component.html - 72 + 52 Subscriptions base path input setting placeholder @@ -710,7 +670,7 @@ Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder. app/settings/settings.component.html - 73 + 53 Subscriptions base path setting input hint @@ -718,7 +678,7 @@ Check interval app/settings/settings.component.html - 78 + 58 Check interval input setting placeholder @@ -726,51 +686,15 @@ Unit is seconds, only include numbers. app/settings/settings.component.html - 79 + 59 Check interval setting input hint - - Use youtube-dl archive - - app/settings/settings.component.html - 83 - - - app/settings/settings.component.html - 152 - - Use youtube-dl archive setting - - - With youtube-dl's archive - - app/settings/settings.component.html - 84 - - youtube-dl archive explanation prefix link - - - feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory. - - app/settings/settings.component.html - 84 - - youtube-dl archive explanation middle - - - This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss. - - app/settings/settings.component.html - 85 - - youtube-dl archive explanation suffix - Theme app/settings/settings.component.html - 94 + 69 Theme select label @@ -778,7 +702,7 @@ Default app/settings/settings.component.html - 96 + 71 Default theme label @@ -786,7 +710,7 @@ Dark app/settings/settings.component.html - 97 + 72 app/app.component.html @@ -798,7 +722,7 @@ Allow theme change app/settings/settings.component.html - 102 + 77 Allow theme change setting @@ -806,7 +730,7 @@ Language app/settings/settings.component.html - 111 + 86 Language select label @@ -822,7 +746,7 @@ Audio folder path app/settings/settings.component.html - 131 + 106 Audio folder path input placeholder @@ -830,7 +754,7 @@ Path for audio only downloads. It is relative to YTDL-Material's root folder. app/settings/settings.component.html - 132 + 107 Aduio path setting input hint @@ -838,7 +762,7 @@ Video folder path app/settings/settings.component.html - 138 + 113 Video folder path input placeholder @@ -846,7 +770,7 @@ Path for video downloads. It is relative to YTDL-Material's root folder. app/settings/settings.component.html - 139 + 114 Video path setting input hint @@ -854,23 +778,47 @@ Global custom args for downloads on the home page. Args are delimited using two commas like so: ,, app/settings/settings.component.html - 146 + 121 Custom args setting input hint - - Safe download override + + Use youtube-dl archive app/settings/settings.component.html - 157 + 127 + + Use youtubedl archive setting + + + Include thumbnail + + app/settings/settings.component.html + 131 - Safe download override setting + Include thumbnail setting + + + Include metadata + + app/settings/settings.component.html + 135 + + Include metadata setting + + + Kill all downloads + + app/settings/settings.component.html + 139 + + Kill all downloads button Downloader app/settings/settings.component.html - 124 + 99 Downloader settings label @@ -878,7 +826,7 @@ Top title app/settings/settings.component.html - 170 + 152 Top title input placeholder @@ -886,7 +834,7 @@ File manager enabled app/settings/settings.component.html - 175 + 157 File manager enabled setting @@ -894,7 +842,7 @@ Downloads manager enabled app/settings/settings.component.html - 178 + 160 Downloads manager enabled setting @@ -902,7 +850,7 @@ Allow quality select app/settings/settings.component.html - 181 + 163 Allow quality seelct setting @@ -910,7 +858,7 @@ Download only mode app/settings/settings.component.html - 184 + 166 Download only mode setting @@ -918,31 +866,15 @@ Allow multi-download mode app/settings/settings.component.html - 187 + 169 Allow multi-download mode setting - - Require pin for settings - - app/settings/settings.component.html - 190 - - Require pin for settings setting - - - Set New Pin - - app/settings/settings.component.html - 191 - - Set new pin button - Enable Public API app/settings/settings.component.html - 199 + 177 Enable Public API key setting @@ -950,7 +882,7 @@ Public API Key app/settings/settings.component.html - 204 + 182 Public API Key setting placeholder @@ -958,7 +890,7 @@ View documentation app/settings/settings.component.html - 205 + 183 View API docs setting hint @@ -966,15 +898,23 @@ Generate app/settings/settings.component.html - 209 + 187 Generate key button + + This will delete your old API key! + + app/settings/settings.component.html + 187 + + delete api key tooltip + Use YouTube API app/settings/settings.component.html - 218 + 196 Use YouTube API setting @@ -982,7 +922,7 @@ Youtube API Key app/settings/settings.component.html - 222 + 200 Youtube API Key setting placeholder @@ -990,7 +930,7 @@ Generating a key is easy! app/settings/settings.component.html - 223 + 201 Youtube API Key setting hint @@ -998,11 +938,11 @@ Click here app/settings/settings.component.html - 233 + 211 app/settings/settings.component.html - 239 + 217 app/dialogs/about-dialog/about-dialog.component.html @@ -1014,7 +954,7 @@ to download the official YoutubeDL-Material Chrome extension manually. app/settings/settings.component.html - 233 + 211 Chrome click here suffix @@ -1022,7 +962,7 @@ You must manually load the extension and modify the extension's settings to set the frontend URL. app/settings/settings.component.html - 234 + 212 Chrome setup suffix @@ -1030,7 +970,7 @@ to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page. app/settings/settings.component.html - 239 + 217 Firefox click here suffix @@ -1038,7 +978,7 @@ Detailed setup instructions. app/settings/settings.component.html - 240 + 218 Firefox setup prefix link @@ -1046,7 +986,7 @@ Not much is required other than changing the extension's settings to set the frontend URL. app/settings/settings.component.html - 240 + 218 Firefox setup suffix @@ -1054,7 +994,7 @@ Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark. app/settings/settings.component.html - 245 + 223 Bookmarklet instructions @@ -1062,7 +1002,7 @@ Generate 'audio only' bookmarklet app/settings/settings.component.html - 246 + 224 Generate audio only bookmarklet checkbox @@ -1070,7 +1010,7 @@ Extra app/settings/settings.component.html - 164 + 146 Extra settings label @@ -1078,7 +1018,7 @@ Use default downloading agent app/settings/settings.component.html - 260 + 238 Use default downloading agent setting @@ -1086,7 +1026,7 @@ Select a downloader app/settings/settings.component.html - 264 + 242 Custom downloader select label @@ -1094,15 +1034,23 @@ Select a logger level app/settings/settings.component.html - 278 + 256 Logger level select label + + Login expiration + + app/settings/settings.component.html + 268 + + Login expiration select label + Allow advanced download app/settings/settings.component.html - 289 + 279 Allow advanced downloading setting @@ -1110,7 +1058,7 @@ Use Cookies app/settings/settings.component.html - 297 + 287 Use cookies setting @@ -1118,7 +1066,7 @@ Set Cookies app/settings/settings.component.html - 298 + 288 Set cookies button @@ -1126,7 +1074,7 @@ Advanced app/settings/settings.component.html - 255 + 233 Host settings label @@ -1134,15 +1082,79 @@ Allow user registration app/settings/settings.component.html - 310 + 302 Allow registration setting + + Internal + + app/settings/settings.component.html + 308 + + Internal auth method + + + LDAP + + app/settings/settings.component.html + 311 + + LDAP auth method + + + Auth method + + app/settings/settings.component.html + 306 + + Auth method select + + + LDAP URL + + app/settings/settings.component.html + 318 + + LDAP URL + + + Bind DN + + app/settings/settings.component.html + 323 + + Bind DN + + + Bind Credentials + + app/settings/settings.component.html + 328 + + Bind Credentials + + + Search Base + + app/settings/settings.component.html + 333 + + Search Base + + + Search Filter + + app/settings/settings.component.html + 338 + + Search Filter + Users app/settings/settings.component.html - 308 + 298 Users settings label @@ -1150,7 +1162,7 @@ Logs app/settings/settings.component.html - 314 + 346 Logs settings label @@ -1158,7 +1170,15 @@ Save app/settings/settings.component.html - 327 + 359 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 58 + + + app/components/modify-users/modify-users.component.html + 58 Settings save button @@ -1166,7 +1186,7 @@ {VAR_SELECT, select, true {Close} false {Cancel} other {otha} } app/settings/settings.component.html - 330 + 362 Settings cancel and close button @@ -1362,6 +1382,14 @@ Navigation menu Downloads Page title + + + + app/app.component.html + 49 + + Navigation menu Downloads Page title + Share playlist @@ -1416,10 +1444,6 @@ app/dialogs/share-media-dialog/share-media-dialog.component.html 24 - - app/components/logs-viewer/logs-viewer.component.html - 7 - Copy to clipboard button @@ -1430,6 +1454,22 @@ Playlist save changes button + + The download was successful + + app/download-item/download-item.component.html + 8 + + download successful tooltip + + + An error has occurred + + app/download-item/download-item.component.html + 9 + + download error tooltip + Details @@ -1500,6 +1540,10 @@ app/dialogs/subscribe-dialog/subscribe-dialog.component.html 23 + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 7 + Download all uploads subscription setting @@ -1508,13 +1552,21 @@ app/dialogs/subscribe-dialog/subscribe-dialog.component.html 26 + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 10 + Download time range prefix Audio-only mode app/dialogs/subscribe-dialog/subscribe-dialog.component.html - 38 + 40 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 24 Streaming-only mode @@ -1522,7 +1574,11 @@ Streaming-only mode app/dialogs/subscribe-dialog/subscribe-dialog.component.html - 43 + 45 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 29 Streaming-only mode @@ -1530,7 +1586,11 @@ These are added after the standard args. app/dialogs/subscribe-dialog/subscribe-dialog.component.html - 51 + 53 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 37 Custom args hint @@ -1538,7 +1598,11 @@ Custom file output app/dialogs/subscribe-dialog/subscribe-dialog.component.html - 57 + 59 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 43 Subscription custom file output placeholder @@ -1546,7 +1610,7 @@ Subscribe app/dialogs/subscribe-dialog/subscribe-dialog.component.html - 72 + 74 Subscribe button @@ -1614,6 +1678,14 @@ No channel subscriptions text + + Playlists + + app/subscriptions/subscriptions.component.html + 27 + + Subscriptions playlists title + Name not available. Playlist retrieval in progress. @@ -1630,12 +1702,28 @@ No playlist subscriptions text + + Editing + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 1 + + Edit subscription dialog title + Search app/subscription/subscription/subscription.component.html 32 + + app/components/modify-users/modify-users.component.html + 7 + + + app/components/recent-videos/recent-videos.component.html + 24 + Subscription videos search placeholder @@ -1652,6 +1740,10 @@ app/subscription/subscription-file-card/subscription-file-card.component.html 8 + + app/components/unified-file-card/unified-file-card.component.html + 23 + Delete and redownload subscription video button @@ -1660,6 +1752,10 @@ app/subscription/subscription-file-card/subscription-file-card.component.html 9 + + app/components/unified-file-card/unified-file-card.component.html + 26 + Delete forever subscription video button @@ -1706,6 +1802,14 @@ Current session + + Clear all downloads + + app/components/downloads/downloads.component.html + 18 + + clear all downloads action button + No downloads available! @@ -1736,6 +1840,10 @@ app/components/manage-user/manage-user.component.html 1 + + app/components/modify-users/modify-users.component.html + 70 + Manage user dialog title @@ -1762,13 +1870,13 @@ Set new password - - Use default + + Use role default app/components/manage-user/manage-user.component.html 19 - Use default + Use role default Yes @@ -1826,6 +1934,22 @@ Actions users table header + + Edit user + + app/components/modify-users/modify-users.component.html + 66 + + edit user action button tooltip + + + Delete user + + app/components/modify-users/modify-users.component.html + 73 + + delete user action button tooltip + Add Users @@ -1842,21 +1966,53 @@ Edit role - - Logs will appear here + + Lines: app/components/logs-viewer/logs-viewer.component.html - 5 + 22 - Logs placeholder + Label for lines select in logger view - - Lines: + + Clear logs app/components/logs-viewer/logs-viewer.component.html - 9 + 34 - Label for lines select in logger view + Clear logs button + + + Open file + + app/components/unified-file-card/unified-file-card.component.html + 13 + + Open file button + + + Open file in new tab + + app/components/unified-file-card/unified-file-card.component.html + 14 + + Open file in new tab + + + Go to subscription + + app/components/unified-file-card/unified-file-card.component.html + 20 + + Go to subscription menu item + + + My videos + + app/components/recent-videos/recent-videos.component.html + 20 + + My videos title From 37a19eabe65123bed5c5164a70ee938efba38405 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 30 Sep 2020 04:34:52 -0400 Subject: [PATCH 21/26] Fixed bug in source translation file --- src/assets/i18n/messages.en.xlf | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/assets/i18n/messages.en.xlf b/src/assets/i18n/messages.en.xlf index 6d2ddf9..86f2fc6 100644 --- a/src/assets/i18n/messages.en.xlf +++ b/src/assets/i18n/messages.en.xlf @@ -1382,14 +1382,6 @@ Navigation menu Downloads Page title - - - - app/app.component.html - 49 - - Navigation menu Downloads Page title - Share playlist From 79b4b993f8129608eb96d945daaf4b69357e6b26 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 30 Sep 2020 04:41:15 -0400 Subject: [PATCH 22/26] Fixed bug in translation source file (2) --- src/app/app.component.html | 2 +- .../edit-subscription-dialog.component.html | 2 +- src/assets/i18n/messages.en.xlf | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) 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/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/assets/i18n/messages.en.xlf b/src/assets/i18n/messages.en.xlf index 86f2fc6..7101d02 100644 --- a/src/assets/i18n/messages.en.xlf +++ b/src/assets/i18n/messages.en.xlf @@ -1694,13 +1694,13 @@ No playlist subscriptions text - - Editing + + Editing app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html 1 - Edit subscription dialog title + Edit subscription dialog title prefix Search From 7dcc38c26d12f0240e1593ba20899fea2b98c8ba Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 30 Sep 2020 04:52:43 -0400 Subject: [PATCH 23/26] Updated string in settings: "Select a logger level" -> "Log Level" Made modify playlist component fully translatable Fixed typo in cookies settings text --- .../cookies-uploader-dialog.component.html | 2 +- .../modify-playlist.component.html | 4 +- src/app/settings/settings.component.html | 2 +- src/assets/i18n/messages.en.xlf | 54 +++++++++++-------- 4 files changed, 37 insertions(+), 25 deletions(-) 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/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/settings/settings.component.html b/src/app/settings/settings.component.html index 719683b..c81ca82 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -253,7 +253,7 @@
- Select a logger level + Log Level Debug Verbose diff --git a/src/assets/i18n/messages.en.xlf b/src/assets/i18n/messages.en.xlf index 7101d02..e7dbf64 100644 --- a/src/assets/i18n/messages.en.xlf +++ b/src/assets/i18n/messages.en.xlf @@ -470,6 +470,34 @@ Modify playlist dialog title + + Add more content + + app/dialogs/modify-playlist/modify-playlist.component.html + 17 + + Add more content + + + Save + + app/dialogs/modify-playlist/modify-playlist.component.html + 27 + + + app/settings/settings.component.html + 359 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 58 + + + app/components/modify-users/modify-users.component.html + 58 + + Save + ID: @@ -570,8 +598,8 @@ Drag and Drop - - 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. app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html 20 @@ -1030,13 +1058,13 @@ Custom downloader select label - - Select a logger level + + Log Level app/settings/settings.component.html 256 - Logger level select label + Log Level label Login expiration @@ -1166,22 +1194,6 @@ Logs settings label - - Save - - app/settings/settings.component.html - 359 - - - app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html - 58 - - - app/components/modify-users/modify-users.component.html - 58 - - Settings save button - {VAR_SELECT, select, true {Close} false {Cancel} other {otha} } From 44445f0b677d563f61bc2de1f7ef37a4ec6d692f Mon Sep 17 00:00:00 2001 From: Tzahi12345 Date: Thu, 1 Oct 2020 14:46:04 -0400 Subject: [PATCH 24/26] Added code analysis GH action --- .github/workflows/codeql-analysis.yml | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml 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 From 8366089444066c399c435f33d849e309f882a5a6 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Fri, 2 Oct 2020 03:05:18 -0400 Subject: [PATCH 25/26] Added support for French, Chinese (Mandarin, simplified), and Norweigan. Updated German and Spanish translations - Updated README to reflect new official translator - XLIFFs to come later --- README.md | 1 + src/app/settings/locales_list.ts | 2 +- src/app/settings/settings.component.ts | 2 +- src/assets/i18n/messages.de.json | 57 ++++-- src/assets/i18n/messages.es.json | 56 ++++-- src/assets/i18n/messages.fr.json | 228 +++++++++++++++++++++++ src/assets/i18n/messages.nb.json | 222 +++++++++++++++++++++++ src/assets/i18n/messages.zh.json | 242 +++++++++++++++++++++++++ 8 files changed, 780 insertions(+), 30 deletions(-) create mode 100644 src/assets/i18n/messages.fr.json create mode 100644 src/assets/i18n/messages.nb.json create mode 100644 src/assets/i18n/messages.zh.json diff --git a/README.md b/README.md index 1059d20..1c22952 100644 --- a/README.md +++ b/README.md @@ -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/src/app/settings/locales_list.ts b/src/app/settings/locales_list.ts index cd69919..d4531aa 100644 --- a/src/app/settings/locales_list.ts +++ b/src/app/settings/locales_list.ts @@ -190,7 +190,7 @@ export const isoLangs = { }, 'fr': { 'name': 'French', - 'nativeName': 'français, langue française' + 'nativeName': 'français' }, 'ff': { 'name': 'Fula; Fulah; Pulaar; Pular', diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 4a7684f..37b14d0 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -17,7 +17,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo }) export class SettingsComponent implements OnInit { all_locales = isoLangs; - supported_locales = ['en', 'es', 'de']; + supported_locales = ['en', 'es', 'de', 'fr', 'zh', 'nb']; initialLocale = localStorage.getItem('locale'); initial_config = null; diff --git a/src/assets/i18n/messages.de.json b/src/assets/i18n/messages.de.json index 8299e65..c9661e1 100644 --- a/src/assets/i18n/messages.de.json +++ b/src/assets/i18n/messages.de.json @@ -1,5 +1,5 @@ { - "17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Playlist erstellen", + "17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Wiedergabeliste erstellen", "cff1428d10d59d14e45edec3c735a27b5482db59": "Name", "f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiodateien", "a52dae09be10ca3a65da918533ced3d3f4992238": "Videos", @@ -35,7 +35,7 @@ "c32ef07f8803a223a83ed17024b38e8d82292407": "Passwort", "4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio", "9779715ac05308973d8f1c8658b29b986e92450f": "Ihre Audiodateien befinden sich hier", - "47546e45bbb476baaaad38244db444c427ddc502": "Playlisten", + "47546e45bbb476baaaad38244db444c427ddc502": "Wiedergabelisten", "78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Keine Wiedergabelisten verfügbar. Erstellen Sie eine aus Ihren heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.", "9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Video", "960582a8b9d7942716866ecfb7718309728f2916": "Ihre Videodateien befinden sich hier", @@ -83,7 +83,7 @@ "46826331da1949bd6fb74624447057099c9d20cd": "Video Basispfad", "17c92e6d47a213fa95b5aa344b3f258147123f93": "Dateipfad für Video-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.", "6b995e7130b4d667eaab6c5f61b362ace486d26d": "Globale benutzerdefinierte Argumente für Downloads auf der Startseite. Argumente werden durch zwei Kommata voneinander getrennt: ,,", - "0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader", + "0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Herunterlader", "61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Titel der Kopfzeile", "78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Dateimanager aktivieren", "a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Download-Manager aktivieren", @@ -117,14 +117,14 @@ "52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Speichern", "fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Schließen} false {Abbrechen} other {Andere}}", "cec82c0a545f37420d55a9b6c45c20546e82f94e": "Über YoutubeDL-Material", - "199c17e5d6a419313af3c325f06dcbb9645ca618": "ist ein Open-Source YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.", + "199c17e5d6a419313af3c325f06dcbb9645ca618": "ist ein quelloffener YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.", "bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "beinhaltet viele tolle Funktionen! API, Docker und Lokalisierung werden unter anderem unterstützt. Informieren Sie sich über alle unterstützten Funktionen auf Github.", "a45e3b05f0529dc5246d70ef62304c94426d4c81": "Installierte Version:", "e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Suche nach Updates ...", "a16e92385b4fd9677bb830a4b796b8b79c113290": "Aktualisierung verfügbar", "189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Sie können über das Einstellungsmenü aktualisieren.", "b33536f59b94ec935a16bd6869d836895dc5300c": "Haben Sie einen Fehler gefunden oder einen Vorschlag?", - "e1f398f38ff1534303d4bb80bd6cece245f24016": "um ein Ticket zu öffnen.", + "e1f398f38ff1534303d4bb80bd6cece245f24016": "um ein Ticket zu öffnen!", "42ff677ec14f111e88bd6cdd30145378e994d1bf": "Ihr Profil", "ac9d09de42edca1296371e4d801349c9096ac8de": "UID:", "a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Erstellt:", @@ -138,8 +138,8 @@ "004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Über", "92eee6be6de0b11c924e3ab27db30257159c0a7c": "Startseite", "357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements", - "822fab38216f64e8166d368b59fe756ca39d301b": "Downloads", - "a249a5ae13e0835383885aaf697d2890cc3e53e9": "Playlist teilen", + "822fab38216f64e8166d368b59fe756ca39d301b": "Heruntergeladene", + "a249a5ae13e0835383885aaf697d2890cc3e53e9": "Wiedergabeliste teilen", "15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Video teilen", "1d540dcd271b316545d070f9d182c372d923aadd": "Audio teilen", "1f6d14a780a37a97899dc611881e6bc971268285": "Freigabe aktivieren", @@ -152,8 +152,8 @@ "77b0c73840665945b25bd128709aa64c8f017e1c": "Download Start:", "08ff9375ec078065bcdd7637b7ea65fce2979266": "Download Ende:", "ad127117f9471612f47d01eae09709da444a36a4": "Dateipfad(e):", - "a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Playlist oder einen Kanal", - "93efc99ae087fc116de708ecd3ace86ca237cf30": "Playlist oder Kanal URL", + "a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Wiedergabeliste oder einen Kanal", + "93efc99ae087fc116de708ecd3ace86ca237cf30": "URL der Wiedergabeliste oder des Kanales", "08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Benutzerdefinierter Name", "f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Dies ist optional", "ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle Uploads herunterladen", @@ -168,18 +168,18 @@ "807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanäle", "29b89f751593e1b347eef103891b7a1ff36ec03f": "Name nicht verfügbar. Kanal wird abgerufen...", "4636cd4a1379c50d471e98786098c4d39e1e82ad": "Sie haben keine Kanäle abonniert.", - "2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Playlist wird abgerufen...", - "587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Playlisten abonniert.", + "2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Wiedergabeliste wird abgerufen.", + "587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Wiedergabeliste abonniert.", "7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Suchen", "2054791b822475aeaea95c0119113de3200f5e1c": "Länge:", "94e01842dcee90531caa52e4147f70679bac87fe": "Löschen und erneut herunterladen", "2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent löschen", - "91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater", + "91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Aktualisierungsprogramm", "1372e61c5bd06100844bd43b98b016aabc468f62": "Wählen Sie eine Version:", "cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrieren", "a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sitzungs-ID:", "eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(aktuell)", - "7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Downloads verfügbar.", + "7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Herunterladen-Ereignisse verfügbar!", "b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren", "024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername", "2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten", @@ -194,5 +194,32 @@ "52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle", "59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Aktionen", "4d92a0395dd66778a931460118626c5794a3fc7a": "Benutzer hinzufügen", - "b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rolle bearbeiten" -} + "b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rolle bearbeiten", + "5009630cdf32ab4f1c78737b9617b8773512c05a": "Zeilen:", + "fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Logs erscheinen hier", + "98b6ec9ec138186d663e64770267b67334353d63": "Benutzerdefinierte Dateiausgabe", + "f432e1a8d6adb12e612127978ce2e0ced933959c": "Diese werden nach den Standardargumenten hinzugefügt.", + "c76a955642714b8949ff3e4b4990864a2e2cac95": "Nur-Audio Modus", + "eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Protokolle", + "80651a7ad1229ea6613557d3559f702cfa5aecf5": "Cookies setzen", + "431e5f3a0dde88768d1074baedd65266412b3f02": "Cookies verwenden", + "d01715b75228878a773ae6d059acc639d4898a03": "Safe download aufheben", + "85e0725c870b28458fd3bbba905397d890f00a69": "Beachte: Neu hochgeladene Cookies überschreiben die vorherigen. Cookies sind global und gelten nicht auf einer Pro-Benutzer-Basis.", + "98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Ziehen-und-Ablegen", + "ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Neue Cookies hochladen", + "28f86ffd419b869711aa13f5e5ff54be6d70731c": "Bearbeiten", + "4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Wiedergabeliste bearbeiten", + "e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Suchfilter", + "4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Intern", + "db6c192032f4cab809aad35215f0aa4765761897": "Ablauf der Anmeldung", + "0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Protokollebene", + "00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Dadurch wird Ihr alter API-Schlüssel gelöscht!", + "fb35145bfb84521e21b6385363d59221f436a573": "Alle Herunterladen-Ereignisse abbrechen", + "384de8f8f112c9e6092eb2698706d391553f3e8d": "Metadaten einschließen", + "ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Miniaturansicht einschließen", + "a8b7b9c168fd936a75e500806a8c0d7755ef1198": "HINWEIS: Durch das Hochladen neuer Cookies werden Ihre vorherigen Cookies überschrieben. Beachten Sie auch, dass Cookies instanzweit und nicht pro Benutzer sind.", + "511b600ae4cf037e4eb3b7a58410842cd5727490": "Weitere Inhalte hinzufügen", + "f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Typ", + "2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Video", + "f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio" +} \ No newline at end of file diff --git a/src/assets/i18n/messages.es.json b/src/assets/i18n/messages.es.json index 674b1ff..91585e4 100644 --- a/src/assets/i18n/messages.es.json +++ b/src/assets/i18n/messages.es.json @@ -197,19 +197,49 @@ "59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ", "4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios", "b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol", - "4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modify playlist", - "28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editar", - "ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Sube nuevas cookies", - "98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrastrar y soltar", - "85e0725c870b28458fd3bbba905397d890f00a69": "NOTA: Cargar nuevas cookies anulará sus cookies anteriores. También tenga en cuenta que las cookies son de toda la instancia, no por usuario.", - "d01715b75228878a773ae6d059acc639d4898a03": "Anulación de descarga segura", - "00e274c496b094a019f0679c3fab3945793f3335": "Seleccione un nivel de registrador", - "431e5f3a0dde88768d1074baedd65266412b3f02": "Utilizar Cookies", - "80651a7ad1229ea6613557d3559f702cfa5aecf5": "Establecer Cookies", "eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registros", - "c76a955642714b8949ff3e4b4990864a2e2cac95": "Solo audio", + "cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Search Base", + "e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Search Filter", + "080cc6abcba236390fc22e79792d0d3443a3bd2a": "Bind Credentials", + "f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Bind DN", + "1db9789b93069861019bd0ccaa5d4706b00afc61": "URL LDAP", + "fa548cee6ea11c160a416cac3e6bdec0363883dc": "Método de autenticación", + "e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP", + "4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Interno", + "80651a7ad1229ea6613557d3559f702cfa5aecf5": "Configurar Cookies", + "431e5f3a0dde88768d1074baedd65266412b3f02": "Usar Cookies", + "db6c192032f4cab809aad35215f0aa4765761897": "Caducidad de inicio de sesión", + "00e274c496b094a019f0679c3fab3945793f3335": "Seleccione un nivel de registrador", + "00a94f58d9eb2e3aa561440eabea616d0c937fa2": "¡Esto eliminará su vieja clave API!", + "fb35145bfb84521e21b6385363d59221f436a573": "Mata todas las descargas", + "384de8f8f112c9e6092eb2698706d391553f3e8d": "Incluir metadatos", + "ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Incluir miniatura", + "85e0725c870b28458fd3bbba905397d890f00a69": "NOTA: La carga de cookies nuevas anulará las cookies anteriores y las cookies son para toda la instancia, no por usuario.", + "98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrastrar y soltar", + "ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Subir nuevas cookies", + "28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editar", + "4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modificar lista de reproducción", + "f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Tipo", + "2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Vídeo", + "f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio", + "d02888c485d3aeab6de628508f4a00312a722894": "Mis videos", + "a0720c36ee1057e5c54a86591b722485c62d7b1a": "Ir a suscripción", + "5656a06f17c24b2d7eae9c221567b209743829a9": "Abrir archivo en nueva pestaña", + "ccf5ea825526ac490974336cb5c24352886abc07": "Abrir archivo", + "8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Borrar registros", + "5009630cdf32ab4f1c78737b9617b8773512c05a": "Líneas:", + "95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Eliminar usuario", + "632e8b20c98e8eec4059a605a4b011bb476137af": "Editar usuario", + "98b6ec9ec138186d663e64770267b67334353d63": "Salida de archivo personalizado", "f432e1a8d6adb12e612127978ce2e0ced933959c": "Estos se agregan después de los argumentos estándar.", - "98b6ec9ec138186d663e64770267b67334353d63": "Salida de archivo personalizada", - "fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Los registros aparecerán aquí", - "5009630cdf32ab4f1c78737b9617b8773512c05a": "Líneas:" + "c76a955642714b8949ff3e4b4990864a2e2cac95": "Modo de solo audio", + "e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Un error ha ocurrido:", + "348cc5d553b18e862eb1c1770e5636f6b05ba130": "Un error ha ocurrido", + "4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "La descarga era exitosa", + "544e09cdc99a8978f48521d45f62db0da6dcf742": "Usar rol predeterminado", + "b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Claro todas las descargas", + "3697f8583ea42868aa269489ad366103d94aece7": "Editando", + "0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Nivel de registro", + "a8b7b9c168fd936a75e500806a8c0d7755ef1198": "NOTA: La carga de cookies nuevas anulará las cookies anteriores y las cookies son para toda la instancia, no por usuario.", + "511b600ae4cf037e4eb3b7a58410842cd5727490": "Agregar más contenido" } \ No newline at end of file diff --git a/src/assets/i18n/messages.fr.json b/src/assets/i18n/messages.fr.json new file mode 100644 index 0000000..4fda7c7 --- /dev/null +++ b/src/assets/i18n/messages.fr.json @@ -0,0 +1,228 @@ +{ + "17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Créer une liste de lecture", + "cff1428d10d59d14e45edec3c735a27b5482db59": "Nom", + "f47e2d56dd8a145b2e9599da9730c049d52962a2": "Fichiers audio", + "a52dae09be10ca3a65da918533ced3d3f4992238": "Vidéos", + "d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modifier les args de téléchargement", + "7fc1946abe2b40f60059c6cd19975d677095fd19": "Nouveaux args simulés", + "0b71824ae71972f236039bed43f8d2323e8fd570": "Ajouter un arg", + "c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Rechercher par catégorie", + "9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Utiliser la valeur arg", + "25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Valeur arg", + "7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Ajouter", + "d7b35c384aecd25a516200d6921836374613dfe7": "Annuler", + "b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modifier", + "038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "Youtube Downloader", + "a38ae1082fec79ba1f379978337385a539a28e73": "Résolution", + "4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Utiliser l'URL", + "d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Voir", + "4a9889d36910edc8323d7bab60858ab3da6d91df": "Audio seulement", + "96a01fafe135afc58b0f8071a4ab00234495ce18": "Activer le téléchargement simultané", + "6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Télécharger", + "6a3777f913cf3f288664f0632b9f24794fdcc24e": "Annuler", + "322ed150e02666fe2259c5b4614eac7066f4ffa0": "Système", + "b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Prévisualisation des arguments :", + "4e4c721129466be9c3862294dc40241b64045998": "Utiliser des arguments personnalisés", + "ad2f8ac8b7de7945b80c8e424484da94e597125f": "Arguments personnalisés", + "a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Pas besoin d'inclure l'URL, seulement ce qui suit. Les arguments sont délimités par deux virgules comme suit ,,", + "3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Modifier le chemin de sortie", + "d9c02face477f2f9cdaae318ccee5f89856851fb": "Sortie personnalisée", + "fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentation", + "19d1ae64d94d28a29b2c57ae8671aace906b5401": "Le chemin est relatif au chemin de téléchargement de la config. Ne pas inclure l'extension.", + "8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "S'authentifier", + "08c74dc9762957593b91f6eb5d65efdfc975bf48": "Identifiant", + "c32ef07f8803a223a83ed17024b38e8d82292407": "Mot de Passe", + "4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio", + "9779715ac05308973d8f1c8658b29b986e92450f": "Vos fichiers audio sont ici", + "47546e45bbb476baaaad38244db444c427ddc502": "Listes de lecture", + "78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Aucune liste de lecture disponible. Créez-en une à l'aide du bouton \\\"+\\\" bleu de votre fichier audio.", + "9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Vídéos", + "960582a8b9d7942716866ecfb7718309728f2916": "Vos fichiers vidéos sont ici", + "0f59c46ca29e9725898093c9ea6b586730d0624e": "Aucune liste de lecture disponible. Créez-en une à l'aide du bouton \\\"+\\\" bleu de votre fichier vidéo.", + "616e206cb4f25bd5885fc35925365e43cf5fb929": "Nom :", + "c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL :", + "c6eb45d085384903e53ab001a3513d1de6a1dbac": "Uploader :", + "109c6f4a5e46efb933612ededfaf52a13178b7e0": "Taille du fichier :", + "bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Chemin :", + "a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Mise en ligne :", + "f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Fermer", + "4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modifier la liste de lecture", + "ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID :", + "e684046d73bcee88e82f7ff01e2852789a05fc32": "Compteur :", + "28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editer", + "826b25211922a1b46436589233cb6f1a163d89b7": "Effacer", + "321e4419a943044e674beb55b8039f42a9761ca5": "Informations", + "34504b488c24c27e68089be549f0eeae6ebaf30b": "Supprimer et bannir", + "ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Téléverser de nouveaux cookies", + "98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Glisser-déposer", + "85e0725c870b28458fd3bbba905397d890f00a69": "NOTE : le téléversement de nouveaux cookies remplacera vos cookies précédents. Notez également que les cookies sont par instance, et non par utilisateur.", + "121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Paramètres", + "801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL", + "54c512cca1923ab72faf1a0bd98d3d172469629a": "URL à partir de laquelle cette application sera accessible, sans le port.", + "cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Port", + "22e8f1d0423a3b784fe40fab187b92c06541b577": "Port souhaité. La valeur par défault est 17442.", + "d4477669a560750d2064051a510ef4d7679e2f3e": "Activer Multi-Utilisateurs", + "2eb03565fcdce7a7a67abc277a936a32fcf51557": "Chemin d'enregistrement des utilisateurs", + "a64505c41150663968e277ec9b3ddaa5f4838798": "Chemin racine pour les utilisateurs et leurs vidéos téléchargées.", + "cbe16a57be414e84b6a68309d08fad894df797d6": "Activer le cryptage des données", + "0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Chemin du fichier de certificat", + "736551b93461d2de64b118cf4043eee1d1c2cb2c": "Chemin d'accès au fichier clé", + "4e3120311801c4acd18de7146add2ee4a4417773": "Autoriser les abonnements", + "4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Chemin d'enregistrement des abonnements", + "bc9892814ee2d119ae94378c905ea440a249b84a": "Chemin racine pour les vidéos des chaînes et des listes de lecture auxquelles vous êtes abonné.", + "5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Interval de vérification des abonnements", + "0f56a7449b77630c114615395bbda4cab398efd8": "L'unité est la seconde, écricre uniquement un nombre entier.", + "78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Utiliser youtube-dl archive", + "fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Avec youtube-dl's archive", + "09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "les vidéos téléchargées à partir de vos abonnements sont enregistrées dans un fichier texte dans le sous-répertoire du fichier d'abonnement.", + "29ed79a98fc01e7f9537777598e31dbde3aa7981": "Cela vous permet de supprimer définitivement des vidéos de vos abonnements sans vous désabonner et vous permet d'enregistrer les vidéos que vous avez téléchargées en cas de perte de données.", + "27a56aad79d8b61269ed303f11664cc78bcc2522": "Thème", + "ff7cee38a2259526c519f878e71b964f41db4348": "Default", + "adb4562d2dbd3584370e44496969d58c511ecb63": "Sombre", + "7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Autoriser le changement du thème", + "fe46ccaae902ce974e2441abe752399288298619": "Choix de la langue", + "82421c3e46a0453a70c42900eab51d58d79e6599": "Principal", + "ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Chemin du dossier audio", + "c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Chemin racine pour les téléchargements audio uniquement.", + "46826331da1949bd6fb74624447057099c9d20cd": "Chemin d'accès au dossier vidéo", + "17c92e6d47a213fa95b5aa344b3f258147123f93": "Chemin racine de téléchargement des vidéos.", + "6b995e7130b4d667eaab6c5f61b362ace486d26d": "Arguments personnalisés globaux pour les téléchargements sur la page d'accueil. Les arguments sont délimités par deux virgules comme suit ,,", + "d01715b75228878a773ae6d059acc639d4898a03": "Désactiver le téléchargement sécurisé", + "0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Téléchargements", + "61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Nom de l'application", + "78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Gestionnaire de fichiers activé", + "a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Gestionnaire de téléchargement activé", + "c33bd5392b39dbed36b8e5a1145163a15d45835f": "Permettre la sélection de la résolution", + "bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Empêcher le téléchargement local", + "09d31c803a7252658694e1e3176b97f5655a3fe3": "Autoriser les téléchargements simultanés", + "d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Utiliser un code PIN", + "f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Définir le code PIN", + "1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Activer l'API publique", + "23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Clé API publique", + "41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Voir documentation", + "1b258b258b4cc475ceb2871305b61756b0134f4a": "Générer", + "d5d7c61349f3b0859336066e6d453fc35d334fe5": "Utiliser l'API YouTube", + "ce10d31febb3d9d60c160750570310f303a22c22": "Clé API YouTube", + "8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Générer un mot de passe !", + "9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Cliquez ici", + "7f09776373995003161235c0c8d02b7f91dbc4df": "pour télécharger manuellement l'extension officielle YoutubeDL-Material Chrome.", + "5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Vous devez charger manuellement l'extension et modifier les paramètres de l'extension pour définir l'URL de l'interface.", + "9a2ec6da48771128384887525bdcac992632c863": "pour installer l'extension officielle YoutubeDL-Material Firefox directement depuis la page des extensions Firefox.", + "eb81be6b49e195e5307811d1d08a19259d411f37": "Installation détaillé ici.", + "cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Il suffit de modifier les paramètres de l'extension pour définir l'URL de l'interface.", + "61b81b11aad0b9d970ece2fce18405f07eac69c2": "Faites glisser le lien ci-dessous vers vos favoris, et le tour est joué ! Il suffit de naviguer vers la vidéo YouTube que vous souhaitez télécharger et de cliquer sur le signet.", + "c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Générer un signet audio uniquement", + "d5f69691f9f05711633128b5a3db696783266b58": "Avancés", + "5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Utiliser l'agent de téléchargement par défault", + "ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Sélectionner une méthode de téléchargement", + "00e274c496b094a019f0679c3fab3945793f3335": "Niveau des logs", + "dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Autoriser le téléchargement avancé", + "431e5f3a0dde88768d1074baedd65266412b3f02": "Utiliser les Cookies", + "80651a7ad1229ea6613557d3559f702cfa5aecf5": "Gérer les Cookies", + "bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avancé", + "37224420db54d4bc7696f157b779a7225f03ca9d": "Autoriser l'enregistrement des utilisateurs", + "4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Utilisateurs", + "eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Journaux", + "52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Sauvegarder", + "fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Fermer} false {Annuler}}", + "cec82c0a545f37420d55a9b6c45c20546e82f94e": "Sobre YoutubeDL-Material", + "199c17e5d6a419313af3c325f06dcbb9645ca618": "est un téléchargeur YouTube open source créé selon le graphique \\\"Material Design\\\" de Google. Vous pouvez facilement télécharger vos vidéos préférées sous forme de fichiers vidéo ou audio, et même vous abonner à vos chaînes et listes de lecture préférées pour vous tenir au courant de vos nouvelles vidéos.", + "bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "comporte des caractéristiques étonnantes ! Une API étendue, un support Docker et un support de localisation (traduction). Pour en savoir plus sur toutes les fonctionnalités prises en charge, cliquez sur l'icône GitHub ci-dessus.", + "a45e3b05f0529dc5246d70ef62304c94426d4c81": "Version installée :", + "e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Vérification des mises à jours ...", + "a16e92385b4fd9677bb830a4b796b8b79c113290": "Mise à jour disponible", + "189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Vous pouvez mettre à jour à partir du menu de configuration.", + "b33536f59b94ec935a16bd6869d836895dc5300c": "Avez-vous trouvé une erreur ou avez-vous une suggestion ?", + "e1f398f38ff1534303d4bb80bd6cece245f24016": "pour signaler un problème !", + "42ff677ec14f111e88bd6cdd30145378e994d1bf": "Votre profil", + "ac9d09de42edca1296371e4d801349c9096ac8de": "UID :", + "a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Créé le :", + "fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Vous n'êtes pas identifié.", + "6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Identifiant", + "bb694b49d408265c91c62799c2b3a7e3151c824d": "Déconnexion", + "a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Créer un compte administrateur", + "2d2adf3ca26a676bca2269295b7455a26fd26980": "Aucun compte administrateur détecté. Veuillez définir le mot de passe du compte adminstrateur \\\"admin\\\".", + "70a67e04629f6d412db0a12d51820b480788d795": "Créer", + "994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profil", + "004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "À propos", + "92eee6be6de0b11c924e3ab27db30257159c0a7c": "Accueil", + "357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements", + "822fab38216f64e8166d368b59fe756ca39d301b": "Téléchargements", + "a249a5ae13e0835383885aaf697d2890cc3e53e9": "Partager une liste de lecture", + "15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Partager vidéo", + "1d540dcd271b316545d070f9d182c372d923aadd": "Partager audio", + "1f6d14a780a37a97899dc611881e6bc971268285": "Activer le partage", + "6580b6a950d952df847cb3d8e7176720a740adc8": "Utiliser l'horodatage", + "4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Secondes", + "3a6e5a6aa78ca864f6542410c5dafb6334538106": "Copier dans le presse-papiers", + "5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Sauvegarder", + "4f8b2bb476981727ab34ed40fde1218361f92c45": "Détails", + "e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Une erreur s'est produite :", + "77b0c73840665945b25bd128709aa64c8f017e1c": "Début du téléchargement :", + "08ff9375ec078065bcdd7637b7ea65fce2979266": "Fin du téléchargement :", + "ad127117f9471612f47d01eae09709da444a36a4": "Chemin(s) de fichier :", + "a9806cf78ce00eb2613eeca11354a97e033377b8": "S'abonner à la liste de lecture ou à la chaîne", + "93efc99ae087fc116de708ecd3ace86ca237cf30": "L'URL de la liste de lecture ou de la chaîne", + "08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nom personnalisé", + "ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Télécharger tous les fichiers téléversés", + "28a678e9cabf86e44c32594c43fa0e890135c20f": "Télécharger les dernières vidéos téléversées", + "c76a955642714b8949ff3e4b4990864a2e2cac95": "Extraire le son", + "408ca4911457e84a348cecf214f02c69289aa8f1": "Streaming uniquement", + "f432e1a8d6adb12e612127978ce2e0ced933959c": "Ceux-ci sont ajoutés après les arguments standard.", + "98b6ec9ec138186d663e64770267b67334353d63": "Sortie de fichier personnalisé", + "d0336848b0c375a1c25ba369b3481ee383217a4f": "S'abonner", + "e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Type :", + "a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archive :", + "8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Exporter l'archive", + "3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Se désabonner", + "e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Vos abonnements", + "807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Chaînes", + "29b89f751593e1b347eef103891b7a1ff36ec03f": "Nom non disponible. Le rétablissement des canaux est en cours...", + "4636cd4a1379c50d471e98786098c4d39e1e82ad": "Vous n'êtes abonné à aucune châine.", + "2e0a410652cb07d069f576b61eab32586a18320d": "Ce nom n'est pas disponible. Recherche de chaînes en cours.", + "587b57ced54965d8874c3fd0e9dfedb987e5df04": "Vous n'êtes abonné·e à aucune liste de lectures.", + "7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Chercher", + "2054791b822475aeaea95c0119113de3200f5e1c": "Durée :", + "94e01842dcee90531caa52e4147f70679bac87fe": "Effacer et re-télécharger", + "2031adb51e07a41844e8ba7704b054e98345c9c1": "Supprimer définitivement", + "91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Mettre à jour", + "1372e61c5bd06100844bd43b98b016aabc468f62": "Sélectionnez une version :", + "cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Créer un compte", + "a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID de la session :", + "eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(actual)", + "7117fc42f860e86d983bfccfcf2654e5750f3406": "Pas de téléchargements !", + "b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Ajouter un utilisateur", + "024886ca34a6f309e3e51c2ed849320592c3faaa": "Identifiant", + "2bd201aea09e43fbfd3cd15ec0499b6755302329": "Gérer l'utilisateur", + "29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Identifiant de l'utilisateur :", + "e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nouveau mot de passe", + "6498fa1b8f563988f769654a75411bb8060134b9": "Enregistrer le nouveau mot de passe", + "40da072004086c9ec00d125165da91eaade7f541": "Utilisation par défault", + "4f20f2d5a6882190892e58b85f6ccbedfa737952": "Oui", + "3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Non", + "57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Gérer le groupe", + "746f64ddd9001ac456327cd9a3d5152203a4b93c": "Identifiant", + "52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Groupe", + "59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Gérer", + "4d92a0395dd66778a931460118626c5794a3fc7a": "Ajouter", + "b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Gérer les groupes", + "fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Les enregistrements apparaîtront ici", + "5009630cdf32ab4f1c78737b9617b8773512c05a": "Lignes :", + "348cc5d553b18e862eb1c1770e5636f6b05ba130": "Une erreur s'est produite", + "4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Le téléchargement a réussi", + "fa548cee6ea11c160a416cac3e6bdec0363883dc": "Méthode d'authentification", + "e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Filtre de recherche", + "4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Interne", + "db6c192032f4cab809aad35215f0aa4765761897": "Expiration de la connexion", + "0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Niveau de journal", + "00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Ceci supprimera votre ancienne clé API !", + "fb35145bfb84521e21b6385363d59221f436a573": "Supprimer tous les téléchargements", + "384de8f8f112c9e6092eb2698706d391553f3e8d": "Inclure les métadonnées", + "ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Inclure une miniature", + "a8b7b9c168fd936a75e500806a8c0d7755ef1198": "REMARQUE : le téléversement de nouveaux cookies remplacera vos cookies précédents. Notez également que les cookies sont à l'échelle de l'instance et non par utilisateur.", + "511b600ae4cf037e4eb3b7a58410842cd5727490": "Ajouter plus de contenu", + "f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Type", + "2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Vidéo", + "f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio", + "3697f8583ea42868aa269489ad366103d94aece7": "Édition" +} \ No newline at end of file diff --git a/src/assets/i18n/messages.nb.json b/src/assets/i18n/messages.nb.json new file mode 100644 index 0000000..120f171 --- /dev/null +++ b/src/assets/i18n/messages.nb.json @@ -0,0 +1,222 @@ +{ + "17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Opprett en spilleliste", + "cff1428d10d59d14e45edec3c735a27b5482db59": "Navn", + "f0baeb8b69d120073b6d60d34785889b0c3232c8": "Lyd", + "2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Video", + "f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Type", + "f47e2d56dd8a145b2e9599da9730c049d52962a2": "Lydfiler", + "a52dae09be10ca3a65da918533ced3d3f4992238": "Videoer", + "d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Endre youtube-dl-argumenter", + "7fc1946abe2b40f60059c6cd19975d677095fd19": "Simulerte nye argumenter", + "0b71824ae71972f236039bed43f8d2323e8fd570": "Legg til argument", + "c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Søk etter kategori", + "9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Bruk argument-verdi", + "25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Argument-verdi", + "7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Legg til argument", + "d7b35c384aecd25a516200d6921836374613dfe7": "Avbryt", + "b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Endre", + "a38ae1082fec79ba1f379978337385a539a28e73": "Kvalitet", + "4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Bruk nettadresse", + "d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Vis", + "4a9889d36910edc8323d7bab60858ab3da6d91df": "Kun lyd", + "96a01fafe135afc58b0f8071a4ab00234495ce18": "Multi-nedlastingsmodus", + "6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Last ned", + "6a3777f913cf3f288664f0632b9f24794fdcc24e": "Avbryt", + "322ed150e02666fe2259c5b4614eac7066f4ffa0": "Avansert", + "b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Simulert kommando:", + "4e4c721129466be9c3862294dc40241b64045998": "Bruk egendefinerte argumenter:", + "ad2f8ac8b7de7945b80c8e424484da94e597125f": "Egendefinerte argumenter", + "a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Du trenger ikke å inkludere nettadressen, kun alt etter. Argumenter skilles ved bruk av to komma, slik: ,,", + "3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Bruk defendefinert utdata", + "d9c02face477f2f9cdaae318ccee5f89856851fb": "Egendefinert utdata", + "fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Dokumentasjon", + "19d1ae64d94d28a29b2c57ae8671aace906b5401": "Sti er relativ til oppsettsnedlastingsstien. Ikke inkluder utvidelse.", + "8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Bruk identitetsbekreftelse", + "08c74dc9762957593b91f6eb5d65efdfc975bf48": "Brukernavn", + "c32ef07f8803a223a83ed17024b38e8d82292407": "Passord", + "616e206cb4f25bd5885fc35925365e43cf5fb929": "Navn:", + "c52db455cca9109ee47e1a612c3f4117c09eb71b": "Nettadresse:", + "c6eb45d085384903e53ab001a3513d1de6a1dbac": "Opplaster:", + "109c6f4a5e46efb933612ededfaf52a13178b7e0": "Filstørrelse:", + "bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Sti:", + "a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Opplastingsdato:", + "f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Lukk", + "4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Endre spilleliste", + "511b600ae4cf037e4eb3b7a58410842cd5727490": "Legg til mer innhold", + "52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Lagre", + "ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:", + "e684046d73bcee88e82f7ff01e2852789a05fc32": "Antall:", + "28f86ffd419b869711aa13f5e5ff54be6d70731c": "Rediger", + "826b25211922a1b46436589233cb6f1a163d89b7": "Slett", + "321e4419a943044e674beb55b8039f42a9761ca5": "Info", + "34504b488c24c27e68089be549f0eeae6ebaf30b": "Slett og svartelist", + "ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Last opp nye kaker", + "98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Dra og slipp", + "a8b7b9c168fd936a75e500806a8c0d7755ef1198": "Merk: Oppasting av nye kaker overskriver tidligere. Merk deg også at kaker gjelder for hele instansen, ikke per bruker.", + "121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Innstillinger", + "801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "Nettadresse", + "54c512cca1923ab72faf1a0bd98d3d172469629a": "Nettadressen dette programmet nås fra, uten porten.", + "cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Port", + "22e8f1d0423a3b784fe40fab187b92c06541b577": "Ønsket port. Forvalet er 17442.", + "d4477669a560750d2064051a510ef4d7679e2f3e": "Multi-brukermodus", + "2eb03565fcdce7a7a67abc277a936a32fcf51557": "Bruker-basissti", + "a64505c41150663968e277ec9b3ddaa5f4838798": "Basissti for brukere og deres nedlastede videoer.", + "4e3120311801c4acd18de7146add2ee4a4417773": "Tillat abonnementer", + "4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Abonnements-basissti", + "bc9892814ee2d119ae94378c905ea440a249b84a": "Basissti for videoer fra dine abonnementskanaler og spillelister. Den er relativ til YTDL-Material sin rotmappe.", + "5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Sjekkintervall", + "0f56a7449b77630c114615395bbda4cab398efd8": "I sekunder, kun tall.", + "27a56aad79d8b61269ed303f11664cc78bcc2522": "Drakt", + "ff7cee38a2259526c519f878e71b964f41db4348": "Forvalg", + "adb4562d2dbd3584370e44496969d58c511ecb63": "Mørk", + "7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Tillat draktendring", + "fe46ccaae902ce974e2441abe752399288298619": "Språk", + "82421c3e46a0453a70c42900eab51d58d79e6599": "Generelt", + "ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Lydmappe", + "c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Sti for lydbaserte nedlastinger. Den er relativ til YTDL-Material sin rotmappe.", + "46826331da1949bd6fb74624447057099c9d20cd": "Videomappe", + "17c92e6d47a213fa95b5aa344b3f258147123f93": "Sti for videonedlastinger. Den er relativ til YTDL-Material sin rotmappe.", + "6b995e7130b4d667eaab6c5f61b362ace486d26d": "Egendefinerte argumenter for nedlastninger på hjemmesiden for hele programmet. Argumenter skilles med to komma, slik: ,,", + "78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Bruk youtube-dl-arktivet", + "ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Inkluder miniatyrbilde", + "384de8f8f112c9e6092eb2698706d391553f3e8d": "Inkluder metadata", + "fb35145bfb84521e21b6385363d59221f436a573": "Drep alle nedlastinger", + "0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Nedlaster", + "61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Topptittel", + "78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Filbehandler påskrudd", + "a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Nedlastingsbehandler påskrudd", + "c33bd5392b39dbed36b8e5a1145163a15d45835f": "Tillat kvalitetsvalg", + "bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Modus kun for nedlasting", + "09d31c803a7252658694e1e3176b97f5655a3fe3": "Tillat multi-nedlastingsmodus", + "1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Skru på offentlig API", + "23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Offentlig API-nøkkel", + "41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Vis dokumentasjon", + "1b258b258b4cc475ceb2871305b61756b0134f4a": "Generer", + "00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Dette vil slette din gamle API-nøkkel!", + "d5d7c61349f3b0859336066e6d453fc35d334fe5": "Bruk YouTube-API", + "ce10d31febb3d9d60c160750570310f303a22c22": "YouTube-API-nøkkel", + "8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Å generere en nøkkel er lett!", + "9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Klikk her", + "7f09776373995003161235c0c8d02b7f91dbc4df": "for å laste ned den offisielle Chrome-utvidelsen for YouTubeDL-Material selv.", + "5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Du må manuelt laste ned utvidelsen og endre dens innstillinger for å sette skjermflate-nettadresse.", + "9a2ec6da48771128384887525bdcac992632c863": "for å installere den offisielle Firefox-utvidelsen for YouTubeDL-Material rett fra Firefox sin utvidelsesside.", + "eb81be6b49e195e5307811d1d08a19259d411f37": "Detaljert oppsettsinstruks", + "cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Ikke mye kreves annet enn å endre utvidelses innstillinger for å sette skjermflate-nettadresse.", + "61b81b11aad0b9d970ece2fce18405f07eac69c2": "Dra lenken nedenfor til bokmerker. Naviger til YouTube-videoen du ønsker å laste ned og klikk på bokmerket.", + "c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Genrer \"kun lyd\"-bookmerke", + "d5f69691f9f05711633128b5a3db696783266b58": "Ekstra", + "5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Bruk forvalgt nedlastingsagent", + "ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Velg en nedlaster", + "0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Loggingsnivå", + "db6c192032f4cab809aad35215f0aa4765761897": "Innloggingsutløp", + "dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Tillat avansert nedlasting", + "431e5f3a0dde88768d1074baedd65266412b3f02": "Bruk kaker", + "80651a7ad1229ea6613557d3559f702cfa5aecf5": "Sett kaker", + "bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avansert", + "37224420db54d4bc7696f157b779a7225f03ca9d": "Tillat brukerregistrering", + "4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Intern", + "e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP", + "fa548cee6ea11c160a416cac3e6bdec0363883dc": "Autentiseringsmetode", + "1db9789b93069861019bd0ccaa5d4706b00afc61": "LDAP-nettadresse", + "f50fa6c09c8944aed504f6325f2913ee6c7a296a": "BIND-DN", + "080cc6abcba236390fc22e79792d0d3443a3bd2a": "BIND-identitetsdetaljer", + "cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Søkebase", + "e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Søkefilter", + "4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Brukere", + "eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Logger", + "fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Close} false {Cancel} other {otha} }", + "cec82c0a545f37420d55a9b6c45c20546e82f94e": "Om YouTubeDL-Material", + "199c17e5d6a419313af3c325f06dcbb9645ca618": "er en fri YouTube-nedlaster bygd i henhold til Google sine Materielle spesifikasjoner. Du kan sømløst laste ned dine favorittvideoer som video- eller lydfiler, og tilogmed abonnere på dine favorittkanaler og spillelister for å holde deg oppdatert med nye videoer.", + "bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "har noen flotte funksjoner inkludert. Et vidtfavnende API, Docker-støtte, og lokalisering (oversettelser)-støtte. Les om alle støttede funksjoner ved å klikke på GitHub-ikonet ovenfor.", + "a45e3b05f0529dc5246d70ef62304c94426d4c81": "Installert versjon:", + "e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Ser etter oppdateringer …", + "a16e92385b4fd9677bb830a4b796b8b79c113290": "Oppdatering tilgjengelig", + "189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Du kan oppdatere fra innstillingsmenyen.", + "b33536f59b94ec935a16bd6869d836895dc5300c": "Funnet en feil eller har et forslag å komme med?", + "e1f398f38ff1534303d4bb80bd6cece245f24016": "for å opprette en feilrapport.", + "42ff677ec14f111e88bd6cdd30145378e994d1bf": "Din profil", + "ac9d09de42edca1296371e4d801349c9096ac8de": "UID:", + "a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Opprettet:", + "fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Du er ikke innlogget.", + "6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Logg inn", + "bb694b49d408265c91c62799c2b3a7e3151c824d": "Logg ut", + "a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Opprett administratorkonto", + "2d2adf3ca26a676bca2269295b7455a26fd26980": "Fant ingen administratorkonto. Dette vil opprette og sette passord for en slik konto med brukernavn som «admin».", + "70a67e04629f6d412db0a12d51820b480788d795": "Opprett", + "994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profil", + "004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Om", + "92eee6be6de0b11c924e3ab27db30257159c0a7c": "Hjem", + "357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnementer", + "822fab38216f64e8166d368b59fe756ca39d301b": "Nedlastinger", + "a249a5ae13e0835383885aaf697d2890cc3e53e9": "Del spilleliste", + "15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Del video", + "1d540dcd271b316545d070f9d182c372d923aadd": "Del lyd", + "1f6d14a780a37a97899dc611881e6bc971268285": "Skru på deling", + "6580b6a950d952df847cb3d8e7176720a740adc8": "Bruk tidsstempel", + "4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Sekunder", + "3a6e5a6aa78ca864f6542410c5dafb6334538106": "Kopier til utklippstavle", + "5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Lagre endringer", + "4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Nedlastet", + "348cc5d553b18e862eb1c1770e5636f6b05ba130": "En feil inntraff", + "4f8b2bb476981727ab34ed40fde1218361f92c45": "Detaljer", + "e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "En feil har inntruffet:", + "77b0c73840665945b25bd128709aa64c8f017e1c": "Nedlastingsstart:", + "08ff9375ec078065bcdd7637b7ea65fce2979266": "Nedlastingsslutt:", + "ad127117f9471612f47d01eae09709da444a36a4": "Filsti(er):", + "a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonner på en spilleliste eller kanal", + "93efc99ae087fc116de708ecd3ace86ca237cf30": "Spilleliste- eller kanal-nettadressen", + "08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Egendefinert navn", + "ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Last ned alle opplastinger", + "28a678e9cabf86e44c32594c43fa0e890135c20f": "Last ned videoer oppdatert siste", + "c76a955642714b8949ff3e4b4990864a2e2cac95": "Kun lyd-modus", + "408ca4911457e84a348cecf214f02c69289aa8f1": "Kun strømming-modus", + "f432e1a8d6adb12e612127978ce2e0ced933959c": "Disse legges til etter standard-argumentene.", + "98b6ec9ec138186d663e64770267b67334353d63": "Egendefinert filutdata", + "d0336848b0c375a1c25ba369b3481ee383217a4f": "Abonner", + "e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Type:", + "a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Arkiv:", + "8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Eksporter arkiv", + "3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Opphev abonnement", + "e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Dine abonnementer", + "807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanaler", + "29b89f751593e1b347eef103891b7a1ff36ec03f": "Navn ikke tilgjengelig. Henter kanal …", + "4636cd4a1379c50d471e98786098c4d39e1e82ad": "Du har ingen kanalabonnementer.", + "47546e45bbb476baaaad38244db444c427ddc502": "Spillelister", + "2e0a410652cb07d069f576b61eab32586a18320d": "Navn ikke tilgjengelig. Henter spilleliste …", + "587b57ced54965d8874c3fd0e9dfedb987e5df04": "Du har ingen spillelisteabonnement.", + "3697f8583ea42868aa269489ad366103d94aece7": "Redigering", + "7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Søk", + "2054791b822475aeaea95c0119113de3200f5e1c": "Lengde:", + "94e01842dcee90531caa52e4147f70679bac87fe": "Slett og last ned igjen", + "2031adb51e07a41844e8ba7704b054e98345c9c1": "Slett for alltid", + "91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Oppdaterer", + "1372e61c5bd06100844bd43b98b016aabc468f62": "Velg en versjon:", + "cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Regustrer", + "a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Økt-ID:", + "eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(nåværende)", + "b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Tøm alle nedlastinger", + "7117fc42f860e86d983bfccfcf2654e5750f3406": "Ingen nedlastninger tilgjengelige!", + "b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registrer en bruker", + "024886ca34a6f309e3e51c2ed849320592c3faaa": "Brukernavn", + "2bd201aea09e43fbfd3cd15ec0499b6755302329": "Håndter bruker", + "29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Bruker-UID:", + "e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nytt passord", + "6498fa1b8f563988f769654a75411bb8060134b9": "Sett nytt passord", + "544e09cdc99a8978f48521d45f62db0da6dcf742": "Bruk rolle-forvalg", + "4f20f2d5a6882190892e58b85f6ccbedfa737952": "Ja", + "3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Nei", + "57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Håndter rolle", + "746f64ddd9001ac456327cd9a3d5152203a4b93c": "Brukernavn", + "52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle", + "59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Handlinger", + "632e8b20c98e8eec4059a605a4b011bb476137af": "Rediger bruker", + "95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Slett bruker", + "4d92a0395dd66778a931460118626c5794a3fc7a": "Legg til brukere", + "b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rediger rolle", + "5009630cdf32ab4f1c78737b9617b8773512c05a": "Linjer:", + "8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Tøm logger", + "ccf5ea825526ac490974336cb5c24352886abc07": "Åpne fil", + "5656a06f17c24b2d7eae9c221567b209743829a9": "Åpne fil i ny fane", + "a0720c36ee1057e5c54a86591b722485c62d7b1a": "Gå til abonnement", + "d02888c485d3aeab6de628508f4a00312a722894": "Mine videoer" +} \ No newline at end of file diff --git a/src/assets/i18n/messages.zh.json b/src/assets/i18n/messages.zh.json new file mode 100644 index 0000000..ab8be32 --- /dev/null +++ b/src/assets/i18n/messages.zh.json @@ -0,0 +1,242 @@ +{ + "17f0ea5d2d7a262b0e875acc70475f102aee84e6": "创建播放列表", + "cff1428d10d59d14e45edec3c735a27b5482db59": "名称", + "f47e2d56dd8a145b2e9599da9730c049d52962a2": "音频文件", + "a52dae09be10ca3a65da918533ced3d3f4992238": "视频文件", + "d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "修改youtube-dl参数", + "7fc1946abe2b40f60059c6cd19975d677095fd19": "模拟新参数", + "0b71824ae71972f236039bed43f8d2323e8fd570": "添加参数", + "c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "按类别搜索", + "9eeb91caef5a50256dd87e1c4b7b3e8216479377": "使用参数值", + "25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "参数值", + "7de2451ed3fb8d8b847979bd3f0c740b970f167b": "添加参数", + "d7b35c384aecd25a516200d6921836374613dfe7": "取消", + "b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "修改", + "038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "Youtube下载器", + "a38ae1082fec79ba1f379978337385a539a28e73": "质量", + "4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "使用URL", + "d3f02f845e62cebd75fde451ab8479d2a8ad784d": "查看", + "4a9889d36910edc8323d7bab60858ab3da6d91df": "仅音频", + "96a01fafe135afc58b0f8071a4ab00234495ce18": "多下载模式", + "6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "下载", + "6a3777f913cf3f288664f0632b9f24794fdcc24e": "取消", + "322ed150e02666fe2259c5b4614eac7066f4ffa0": "高级", + "b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "模拟命令:", + "4e4c721129466be9c3862294dc40241b64045998": "使用自定义参数", + "ad2f8ac8b7de7945b80c8e424484da94e597125f": "自定义参数", + "a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "不必指定URL,仅需指定其后的部分。参数用两个逗号分隔:,,", + "3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "使用自定义输出", + "d9c02face477f2f9cdaae318ccee5f89856851fb": "自定义输出", + "fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "文档", + "19d1ae64d94d28a29b2c57ae8671aace906b5401": "该路径是相对于配置下载路径的,省略文件扩展名", + "8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "使用身份验证", + "08c74dc9762957593b91f6eb5d65efdfc975bf48": "用户名", + "c32ef07f8803a223a83ed17024b38e8d82292407": "密码", + "4a0dada6e841a425de3e5006e6a04df26c644fa5": "音频", + "9779715ac05308973d8f1c8658b29b986e92450f": "您的音频文件在这里", + "47546e45bbb476baaaad38244db444c427ddc502": "播放列表", + "78bd81adb4609b68cfa4c589222bdc233ba1faaa": "没有可用的播放列表。 通过单击蓝色加号按钮从您下载的音频文件创建一个。", + "9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "视频", + "960582a8b9d7942716866ecfb7718309728f2916": "您的视频文件在这里", + "0f59c46ca29e9725898093c9ea6b586730d0624e": "没有可用的播放列表。 通过单击蓝色加号按钮,从下载的视频文件中创建一个。", + "616e206cb4f25bd5885fc35925365e43cf5fb929": "名称:", + "c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:", + "c6eb45d085384903e53ab001a3513d1de6a1dbac": "上传者:", + "109c6f4a5e46efb933612ededfaf52a13178b7e0": "文件大小:", + "bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "路径:", + "a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "上传日期:", + "f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "关闭", + "4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "修改播放列表", + "ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:", + "e684046d73bcee88e82f7ff01e2852789a05fc32": "数量:", + "28f86ffd419b869711aa13f5e5ff54be6d70731c": "编辑", + "826b25211922a1b46436589233cb6f1a163d89b7": "删除", + "321e4419a943044e674beb55b8039f42a9761ca5": "详情", + "34504b488c24c27e68089be549f0eeae6ebaf30b": "删除并拉黑", + "ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "上传新Cookies", + "98a8a42e5efffe17ab786636ed0139b4c7032d0e": "拖放", + "85e0725c870b28458fd3bbba905397d890f00a69": "注意:加载新的Cookies将覆盖您以前的Cookie。并且Cookies的范围是整个实例,而不是每个用户单独分开的。", + "121cc5391cd2a5115bc2b3160379ee5b36cd7716": "设置", + "801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL", + "54c512cca1923ab72faf1a0bd98d3d172469629a": "设置访问URL,无需端口。", + "cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "端口", + "22e8f1d0423a3b784fe40fab187b92c06541b577": "设置目标端口。默认为17442。", + "d4477669a560750d2064051a510ef4d7679e2f3e": "多用户模式", + "2eb03565fcdce7a7a67abc277a936a32fcf51557": "用户文件路径", + "a64505c41150663968e277ec9b3ddaa5f4838798": "用户及其下载视频的文件路径。", + "cbe16a57be414e84b6a68309d08fad894df797d6": "使用加密(SSL)", + "0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "证书文件路径", + "736551b93461d2de64b118cf4043eee1d1c2cb2c": "密钥文件路径", + "4e3120311801c4acd18de7146add2ee4a4417773": "允许订阅", + "4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "订阅文件路径", + "bc9892814ee2d119ae94378c905ea440a249b84a": "订阅频道和播放列表中视频的文件路径(相对于根文件夹而言)。", + "5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "检查间隔", + "0f56a7449b77630c114615395bbda4cab398efd8": "单位是秒,只包含数字。", + "78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "使用youtube-dl存档", + "fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "根据youtube-dl的存档功能", + "09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "从您的订阅下载的视频会记录在订阅存档子目录中的文本文件中。", + "29ed79a98fc01e7f9537777598e31dbde3aa7981": "这样一来,您无需取消订阅便可以从订阅中永久删除视频。并且它还可以在数据丢失的情况下记录已经下载了哪些视频。", + "27a56aad79d8b61269ed303f11664cc78bcc2522": "主题", + "ff7cee38a2259526c519f878e71b964f41db4348": "默认", + "adb4562d2dbd3584370e44496969d58c511ecb63": "暗黑", + "7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "允许更改主题", + "fe46ccaae902ce974e2441abe752399288298619": "语言", + "82421c3e46a0453a70c42900eab51d58d79e6599": "常规", + "ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "音频文件夹路径", + "c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "音频下载的文件路径。相对于YTDL-Material的根文件夹。", + "46826331da1949bd6fb74624447057099c9d20cd": "视频文件夹路径", + "17c92e6d47a213fa95b5aa344b3f258147123f93": "视频下载的文件路径。相对于YTDL-Material的根文件夹。", + "6b995e7130b4d667eaab6c5f61b362ace486d26d": "开始页面上用于下载的全局自定义参数。参数由两个逗号分隔:,,", + "d01715b75228878a773ae6d059acc639d4898a03": "安全下载覆盖", + "0ba25ad86a240576c4f20a2fada4722ebba77b1e": "下载程序", + "61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "首页标题", + "78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "启用文件管理", + "a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "启用下载管理", + "c33bd5392b39dbed36b8e5a1145163a15d45835f": "允许选择下载质量", + "bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "仅下载模式", + "09d31c803a7252658694e1e3176b97f5655a3fe3": "开启多下载模式", + "d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "使用PIN码保护设置", + "f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "设置新PIN码", + "1c4dbce56d96b8974aac24a02f7ab2ee81415014": "启用公共API", + "23bd81dcc30b74d06279a26d7a42e8901c1b124e": "公共API密钥", + "41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "查看文档", + "1b258b258b4cc475ceb2871305b61756b0134f4a": "生成", + "d5d7c61349f3b0859336066e6d453fc35d334fe5": "使用YouTube API", + "ce10d31febb3d9d60c160750570310f303a22c22": "Youtube API密钥", + "8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "生成密钥很简单!", + "9b3cedfa83c6d7acb3210953289d1be4aab115c7": "点击这里", + "7f09776373995003161235c0c8d02b7f91dbc4df": "来手动下载官方的YoutubeDL-Material Chrome扩展程序。", + "5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "您必须手动安装扩展,并且在扩展的设置中输入下载器URL。", + "9a2ec6da48771128384887525bdcac992632c863": "直接从Firefox扩展商店安装官方的YoutubeDL-Material Firefox扩展程序。", + "eb81be6b49e195e5307811d1d08a19259d411f37": "详细的扩展说明。", + "cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "只需在扩展的设置中输入前端URL。", + "61b81b11aad0b9d970ece2fce18405f07eac69c2": "只需将下面的链接拖放到书签栏中。在YouTube页面上您只需单击书签即可下载视频。", + "c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "生成“仅音频”书签", + "d5f69691f9f05711633128b5a3db696783266b58": "额外", + "5fab47f146b0a4b809dcebf3db9da94df6299ea1": "使用默认下载代理", + "ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "选择下载器", + "00e274c496b094a019f0679c3fab3945793f3335": "选择日志级别", + "dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "开启高级下载选项", + "431e5f3a0dde88768d1074baedd65266412b3f02": "使用Cookies", + "80651a7ad1229ea6613557d3559f702cfa5aecf5": "设置Cookies", + "bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "高级", + "37224420db54d4bc7696f157b779a7225f03ca9d": "允许用户注册", + "4d13a9cd5ed3dcee0eab22cb25198d43886942be": "用户", + "eb3d5aefff38a814b76da74371cbf02c0789a1ef": "日志", + "52c9a103b812f258bcddc3d90a6e3f46871d25fe": "保存", + "fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {关} false {取消} }", + "cec82c0a545f37420d55a9b6c45c20546e82f94e": "关于 YoutubeDL-Material", + "199c17e5d6a419313af3c325f06dcbb9645ca618": "是根据Google的Material Design规范构建的开源YouTube下载器。您可以将喜欢的视频下载为视频或音频文件,并且可以订阅喜欢的频道和播放列表,以便及时下载他们的新视频。", + "bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "包含很多很棒的功能!支持API,Docker和本地化。在Github上查找所有受支持的功能。", + "a45e3b05f0529dc5246d70ef62304c94426d4c81": "安装版本:", + "e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "检查更新...", + "a16e92385b4fd9677bb830a4b796b8b79c113290": "更新可用", + "189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "您可以从设置菜单进行更新。", + "b33536f59b94ec935a16bd6869d836895dc5300c": "发现了一个错误或有一些建议?", + "e1f398f38ff1534303d4bb80bd6cece245f24016": "创建新issue!", + "42ff677ec14f111e88bd6cdd30145378e994d1bf": "您的个人资料", + "ac9d09de42edca1296371e4d801349c9096ac8de": "UID:", + "a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "创建日期:", + "fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "您尚未登录。", + "6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "登录", + "bb694b49d408265c91c62799c2b3a7e3151c824d": "注销", + "a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "创建管理员帐户", + "2d2adf3ca26a676bca2269295b7455a26fd26980": "未检测到默认管理员帐户。即将创建一个名为admin的管理员帐户并设置密码。", + "70a67e04629f6d412db0a12d51820b480788d795": "创建", + "994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "个人资料", + "004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "关于", + "92eee6be6de0b11c924e3ab27db30257159c0a7c": "首 页", + "357064ca9d9ac859eb618e28e8126fa32be049e2": "订 阅", + "822fab38216f64e8166d368b59fe756ca39d301b": "下 载", + "a249a5ae13e0835383885aaf697d2890cc3e53e9": "分享播放列表", + "15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "分享视频", + "1d540dcd271b316545d070f9d182c372d923aadd": "分享音频", + "1f6d14a780a37a97899dc611881e6bc971268285": "启用共享", + "6580b6a950d952df847cb3d8e7176720a740adc8": "使用时间戳", + "4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "秒", + "3a6e5a6aa78ca864f6542410c5dafb6334538106": "复制到剪贴板", + "5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "保存更改", + "4f8b2bb476981727ab34ed40fde1218361f92c45": "详细", + "e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "发生错误:", + "77b0c73840665945b25bd128709aa64c8f017e1c": "下载开始:", + "08ff9375ec078065bcdd7637b7ea65fce2979266": "下载结束:", + "ad127117f9471612f47d01eae09709da444a36a4": "文件路径:", + "a9806cf78ce00eb2613eeca11354a97e033377b8": "订阅播放列表或频道", + "93efc99ae087fc116de708ecd3ace86ca237cf30": "播放列表或频道URL", + "08f5d0ef937ae17feb1b04aff15ad88911e87baf": "自定义名称", + "ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "下载所有音视频", + "28a678e9cabf86e44c32594c43fa0e890135c20f": "下载最近多久的视频", + "c76a955642714b8949ff3e4b4990864a2e2cac95": "仅音频模式", + "408ca4911457e84a348cecf214f02c69289aa8f1": "仅视频模式", + "f432e1a8d6adb12e612127978ce2e0ced933959c": "这些是在标准参数之后添加的。", + "98b6ec9ec138186d663e64770267b67334353d63": "自定义文件输出", + "d0336848b0c375a1c25ba369b3481ee383217a4f": "订阅", + "e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "类型:", + "a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "存档:", + "8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "导出存档", + "3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "取消订阅", + "e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "您的订阅", + "807cf11e6ac1cde912496f764c176bdfdd6b7e19": "频道", + "29b89f751593e1b347eef103891b7a1ff36ec03f": "名称不可用。正在检索频道...", + "4636cd4a1379c50d471e98786098c4d39e1e82ad": "您尚未订阅任何频道。", + "2e0a410652cb07d069f576b61eab32586a18320d": "名称不可用。正在检索播放列表...", + "587b57ced54965d8874c3fd0e9dfedb987e5df04": "您尚未订阅任何播放列表。", + "7e892ba15f2c6c17e83510e273b3e10fc32ea016": "搜索", + "2054791b822475aeaea95c0119113de3200f5e1c": "长度:", + "94e01842dcee90531caa52e4147f70679bac87fe": "删除并重新下载", + "2031adb51e07a41844e8ba7704b054e98345c9c1": "永久删除", + "91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "更新程序", + "1372e61c5bd06100844bd43b98b016aabc468f62": "选择版本:", + "cfc2f436ec2beffb042e7511a73c89c372e86a6c": "注册", + "a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "会话ID:", + "eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(当前)", + "7117fc42f860e86d983bfccfcf2654e5750f3406": "没有下载可用!", + "b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "注册用户", + "024886ca34a6f309e3e51c2ed849320592c3faaa": "用户名", + "2bd201aea09e43fbfd3cd15ec0499b6755302329": "管理用户", + "29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "用户UID:", + "e70e209561583f360b1e9cefd2cbb1fe434b6229": "新密码", + "6498fa1b8f563988f769654a75411bb8060134b9": "设置新密码", + "40da072004086c9ec00d125165da91eaade7f541": "使用默认值", + "4f20f2d5a6882190892e58b85f6ccbedfa737952": "是", + "3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "否", + "57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "管理用户", + "746f64ddd9001ac456327cd9a3d5152203a4b93c": "用户名", + "52c1447c1ec9570a2a3025c7e566557b8d19ed92": "身份", + "59a8c38db3091a63ac1cb9590188dc3a972acfb3": "动作", + "4d92a0395dd66778a931460118626c5794a3fc7a": "添加用户", + "b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "编辑用户", + "fd59fb984749fcdb5e386ae85faec82f8e5ac098": "日志将出现在这里", + "5009630cdf32ab4f1c78737b9617b8773512c05a": "行:", + "ffc19f32b1cba0daefc0e5668f89346db1db83ad": "包括缩略图", + "f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "类型", + "2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "视频", + "f0baeb8b69d120073b6d60d34785889b0c3232c8": "音频", + "ccf5ea825526ac490974336cb5c24352886abc07": "打开文件", + "8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "清空日志", + "95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "删除用户", + "632e8b20c98e8eec4059a605a4b011bb476137af": "编辑用户", + "b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "清空所有下载", + "080cc6abcba236390fc22e79792d0d3443a3bd2a": "绑定凭证", + "f50fa6c09c8944aed504f6325f2913ee6c7a296a": "绑定DN", + "1db9789b93069861019bd0ccaa5d4706b00afc61": "LDAP链接", + "fa548cee6ea11c160a416cac3e6bdec0363883dc": "认证方式", + "e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP认证", + "4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "内部身份验证", + "db6c192032f4cab809aad35215f0aa4765761897": "登录到期", + "0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "日志等级", + "00a94f58d9eb2e3aa561440eabea616d0c937fa2": "这将删除您的旧API密钥!", + "384de8f8f112c9e6092eb2698706d391553f3e8d": "包含元数据", + "a8b7b9c168fd936a75e500806a8c0d7755ef1198": "注意:加载新的Cookies将覆盖您以前的Cookie。并且Cookies的范围是整个实例,而不是每个用户单独分开的。", + "511b600ae4cf037e4eb3b7a58410842cd5727490": "添加更多内容", + "d02888c485d3aeab6de628508f4a00312a722894": "我的视频", + "a0720c36ee1057e5c54a86591b722485c62d7b1a": "前往订阅", + "5656a06f17c24b2d7eae9c221567b209743829a9": "在新标签页打开文件", + "348cc5d553b18e862eb1c1770e5636f6b05ba130": "出现错误", + "4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "下载成功", + "e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "搜索过滤器", + "cfa67d14d84fe0e9fadf251dc51ffc181173b662": "搜索起点", + "544e09cdc99a8978f48521d45f62db0da6dcf742": "使用角色预设", + "3697f8583ea42868aa269489ad366103d94aece7": "编辑中", + "fb35145bfb84521e21b6385363d59221f436a573": "取消所有下载" +} \ No newline at end of file From edc22cc47b3e5dba47f4bb358c7389963557b8ed Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Fri, 23 Oct 2020 01:38:44 -0400 Subject: [PATCH 26/26] File cards now use the locale to format dates --- .../custom-playlists.component.html | 2 +- .../recent-videos.component.html | 4 +-- .../unified-file-card.component.html | 2 +- .../unified-file-card.component.ts | 16 ++++++++++++ src/app/posts.services.ts | 13 ++++++++++ src/app/settings/locales_list.ts | 25 +++++++++++++------ src/app/settings/settings.component.ts | 2 +- 7 files changed, 52 insertions(+), 12 deletions(-) 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/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 780bee4..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,5 +1,5 @@
-
{{(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}}
(); @Output() goToSubscription = new EventEmitter(); @Output() deleteFile = new EventEmitter(); @Output() editPlaylist = new EventEmitter(); + @ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger; contextMenuPosition = { x: '0px', y: '0px' }; diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 416fd2d..3336a7f 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -11,6 +11,7 @@ import { BehaviorSubject } from 'rxjs'; import { v4 as uuid } from 'uuid'; import { MatSnackBar } from '@angular/material/snack-bar'; import * as Fingerprint2 from 'fingerprintjs2'; +import { isoLangs } from './settings/locales_list'; @Injectable() export class PostsService implements CanActivate { @@ -53,6 +54,7 @@ export class PostsService implements CanActivate { config = null; subscriptions = null; sidenav = null; + locale = isoLangs['en']; constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document, public snackBar: MatSnackBar) { @@ -114,6 +116,17 @@ export class PostsService implements CanActivate { if (localStorage.getItem('card_size')) { this.card_size = localStorage.getItem('card_size'); } + + // localization + const locale = localStorage.getItem('locale'); + if (!locale) { + localStorage.setItem('locale', 'en'); + } + + if (isoLangs[locale]) { + this.locale = isoLangs[locale]; + } + } canActivate(route, state): Promise { return new Promise(resolve => { diff --git a/src/app/settings/locales_list.ts b/src/app/settings/locales_list.ts index d4531aa..5e88b73 100644 --- a/src/app/settings/locales_list.ts +++ b/src/app/settings/locales_list.ts @@ -122,7 +122,8 @@ export const isoLangs = { }, 'zh': { 'name': 'Chinese', - 'nativeName': '中文 (Zhōngwén), 汉语, 漢語' + 'nativeName': '中文 (Zhōngwén), 汉语, 漢語', + 'ngID': 'zh' }, 'cv': { 'name': 'Chuvash', @@ -162,8 +163,14 @@ export const isoLangs = { }, 'en': { 'name': 'English', - 'nativeName': 'English' - }, + 'nativeName': 'English', + 'ngID': 'en-US' + }, + 'en-GB': { + 'name': 'British English', + 'nativeName': 'British English', + 'ngID': 'en-GB' + }, 'eo': { 'name': 'Esperanto', 'nativeName': 'Esperanto' @@ -190,7 +197,8 @@ export const isoLangs = { }, 'fr': { 'name': 'French', - 'nativeName': 'français' + 'nativeName': 'français', + 'ngID': 'fr' }, 'ff': { 'name': 'Fula; Fulah; Pulaar; Pular', @@ -206,7 +214,8 @@ export const isoLangs = { }, 'de': { 'name': 'German', - 'nativeName': 'Deutsch' + 'nativeName': 'Deutsch', + 'ngID': 'de' }, 'el': { 'name': 'Greek, Modern', @@ -438,7 +447,8 @@ export const isoLangs = { }, 'nb': { 'name': 'Norwegian Bokmål', - 'nativeName': 'Norsk bokmål' + 'nativeName': 'Norsk bokmål', + 'ngID': 'nb' }, 'nd': { 'name': 'North Ndebele', @@ -594,7 +604,8 @@ export const isoLangs = { }, 'es': { 'name': 'Spanish; Castilian', - 'nativeName': 'español' + 'nativeName': 'español', + 'ngID': 'es' }, 'su': { 'name': 'Sundanese', diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 37b14d0..9771251 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -17,7 +17,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo }) export class SettingsComponent implements OnInit { all_locales = isoLangs; - supported_locales = ['en', 'es', 'de', 'fr', 'zh', 'nb']; + supported_locales = ['en', 'es', 'de', 'fr', 'zh', 'nb', 'en-GB']; initialLocale = localStorage.getItem('locale'); initial_config = null;