mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-29 16:10:56 +03:00
Compare commits
2 Commits
locale-bas
...
fix-playli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
004a234b02 | ||
|
|
daca715d1b |
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
@@ -1,71 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -111,7 +111,6 @@ If you're interested in translating the app into a new language, check out the [
|
|||||||
Official translators:
|
Official translators:
|
||||||
* Spanish - tzahi12345
|
* Spanish - tzahi12345
|
||||||
* German - UnlimitedCookies
|
* German - UnlimitedCookies
|
||||||
* Chinese - TyRoyal
|
|
||||||
|
|
||||||
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
|
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
|
||||||
|
|
||||||
|
|||||||
432
backend/app.js
432
backend/app.js
@@ -1,6 +1,6 @@
|
|||||||
|
var async = require('async');
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
var fs = require('fs-extra');
|
var fs = require('fs-extra');
|
||||||
var { promisify } = require('util');
|
|
||||||
var auth_api = require('./authentication/auth');
|
var auth_api = require('./authentication/auth');
|
||||||
var winston = require('winston');
|
var winston = require('winston');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
@@ -18,6 +18,7 @@ var utils = require('./utils')
|
|||||||
var mergeFiles = require('merge-files');
|
var mergeFiles = require('merge-files');
|
||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
var ProgressBar = require('progress');
|
var ProgressBar = require('progress');
|
||||||
|
var md5 = require('md5');
|
||||||
const NodeID3 = require('node-id3')
|
const NodeID3 = require('node-id3')
|
||||||
const downloader = require('youtube-dl/lib/downloader')
|
const downloader = require('youtube-dl/lib/downloader')
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
@@ -194,41 +195,35 @@ app.use(auth_api.passport.initialize());
|
|||||||
|
|
||||||
// actual functions
|
// actual functions
|
||||||
|
|
||||||
/**
|
|
||||||
* setTimeout, but its a promise.
|
|
||||||
* @param {number} ms
|
|
||||||
*/
|
|
||||||
async function wait(ms) {
|
|
||||||
await new Promise(resolve => {
|
|
||||||
setTimeout(resolve, ms);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkMigrations() {
|
async function checkMigrations() {
|
||||||
|
return new Promise(async resolve => {
|
||||||
// 3.5->3.6 migration
|
// 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();
|
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) {
|
if (!files_to_db_migration_complete) {
|
||||||
logger.info('Beginning migration: 3.5->3.6+')
|
logger.info('Beginning migration: 3.5->3.6+')
|
||||||
const success = await runFilesToDBMigration()
|
runFilesToDBMigration().then(success => {
|
||||||
if (success) { logger.info('3.5->3.6+ migration complete!'); }
|
if (success) { logger.info('3.5->3.6+ migration complete!'); }
|
||||||
else { logger.error('Migration failed: 3.5->3.6+'); }
|
else { logger.error('Migration failed: 3.5->3.6+'); }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
resolve(true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runFilesToDBMigration() {
|
async function runFilesToDBMigration() {
|
||||||
|
return new Promise(async resolve => {
|
||||||
try {
|
try {
|
||||||
let mp3s = await getMp3s();
|
let mp3s = getMp3s();
|
||||||
let mp4s = await getMp4s();
|
let mp4s = getMp4s();
|
||||||
|
|
||||||
for (let i = 0; i < mp3s.length; i++) {
|
for (let i = 0; i < mp3s.length; i++) {
|
||||||
let file_obj = mp3s[i];
|
let file_obj = mp3s[i];
|
||||||
const file_already_in_db = db.get('files.audio').find({id: file_obj.id}).value();
|
const file_already_in_db = db.get('files.audio').find({id: file_obj.id}).value();
|
||||||
if (!file_already_in_db) {
|
if (!file_already_in_db) {
|
||||||
logger.verbose(`Migrating file ${file_obj.id}`);
|
logger.verbose(`Migrating file ${file_obj.id}`);
|
||||||
await db_api.registerFileDB(file_obj.id + '.mp3', 'audio');
|
db_api.registerFileDB(file_obj.id + '.mp3', 'audio');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,17 +232,18 @@ async function runFilesToDBMigration() {
|
|||||||
const file_already_in_db = db.get('files.video').find({id: file_obj.id}).value();
|
const file_already_in_db = db.get('files.video').find({id: file_obj.id}).value();
|
||||||
if (!file_already_in_db) {
|
if (!file_already_in_db) {
|
||||||
logger.verbose(`Migrating file ${file_obj.id}`);
|
logger.verbose(`Migrating file ${file_obj.id}`);
|
||||||
await db_api.registerFileDB(file_obj.id + '.mp4', 'video');
|
db_api.registerFileDB(file_obj.id + '.mp4', 'video');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sets migration to complete
|
// sets migration to complete
|
||||||
db.set('files_to_db_migration_complete', true).write();
|
db.set('files_to_db_migration_complete', true).write();
|
||||||
return true;
|
resolve(true);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
return false;
|
resolve(false);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
@@ -422,20 +418,20 @@ async function downloadReleaseZip(tag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function installDependencies() {
|
async function installDependencies() {
|
||||||
|
return new Promise(resolve => {
|
||||||
var child_process = require('child_process');
|
var child_process = require('child_process');
|
||||||
var exec = promisify(child_process.exec);
|
child_process.execSync('npm install',{stdio:[0,1,2]});
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
await exec('npm install',{stdio:[0,1,2]});
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function backupServerLite() {
|
async function backupServerLite() {
|
||||||
await fs.ensureDir(path.join(__dirname, 'appdata', 'backups'));
|
return new Promise(async resolve => {
|
||||||
|
fs.ensureDirSync(path.join(__dirname, 'appdata', 'backups'));
|
||||||
let output_path = path.join('appdata', 'backups', `backup-${Date.now()}.zip`);
|
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.`);
|
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));
|
let output = fs.createWriteStream(path.join(__dirname, output_path));
|
||||||
|
|
||||||
await new Promise(resolve => {
|
|
||||||
var archive = archiver('zip', {
|
var archive = archiver('zip', {
|
||||||
gzip: true,
|
gzip: true,
|
||||||
zlib: { level: 9 } // Sets the compression level.
|
zlib: { level: 9 } // Sets the compression level.
|
||||||
@@ -459,53 +455,58 @@ async function backupServerLite() {
|
|||||||
ignore: files_to_ignore
|
ignore: files_to_ignore
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve(archive.finalize());
|
await archive.finalize();
|
||||||
});
|
|
||||||
|
|
||||||
// wait a tiny bit for the zip to reload in fs
|
// wait a tiny bit for the zip to reload in fs
|
||||||
await wait(100);
|
setTimeout(function() {
|
||||||
return true;
|
resolve(true);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isNewVersionAvailable() {
|
async function isNewVersionAvailable() {
|
||||||
|
return new Promise(async resolve => {
|
||||||
// gets tag of the latest version of youtubedl-material, compare to current version
|
// gets tag of the latest version of youtubedl-material, compare to current version
|
||||||
const latest_tag = await getLatestVersion();
|
const latest_tag = await getLatestVersion();
|
||||||
const current_tag = CONSTS['CURRENT_VERSION'];
|
const current_tag = CONSTS['CURRENT_VERSION'];
|
||||||
if (latest_tag > current_tag) {
|
if (latest_tag > current_tag) {
|
||||||
return true;
|
resolve(true);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
resolve(false);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLatestVersion() {
|
async function getLatestVersion() {
|
||||||
const res = await fetch('https://api.github.com/repos/tzahi12345/youtubedl-material/releases/latest', {method: 'Get'});
|
return new Promise(resolve => {
|
||||||
const json = await res.json();
|
fetch('https://api.github.com/repos/tzahi12345/youtubedl-material/releases/latest', {method: 'Get'})
|
||||||
|
.then(async res => res.json())
|
||||||
|
.then(async (json) => {
|
||||||
if (json['message']) {
|
if (json['message']) {
|
||||||
// means there's an error in getting latest version
|
// means there's an error in getting latest version
|
||||||
logger.error(`ERROR: Received the following message from GitHub's API:`);
|
logger.error(`ERROR: Received the following message from GitHub's API:`);
|
||||||
logger.error(json['message']);
|
logger.error(json['message']);
|
||||||
if (json['documentation_url']) logger.error(`Associated URL: ${json['documentation_url']}`)
|
if (json['documentation_url']) logger.error(`Associated URL: ${json['documentation_url']}`)
|
||||||
}
|
}
|
||||||
return json['tag_name'];
|
resolve(json['tag_name']);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function killAllDownloads() {
|
async function killAllDownloads() {
|
||||||
const lookupAsync = promisify(ps.lookup);
|
return new Promise(resolve => {
|
||||||
|
ps.lookup({
|
||||||
try {
|
command: 'youtube-dl',
|
||||||
await lookupAsync({
|
}, function(err, resultList ) {
|
||||||
command: 'youtube-dl'
|
if (err) {
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
// failed to get list of processes
|
// failed to get list of processes
|
||||||
logger.error('Failed to get a list of running youtube-dl processes.');
|
logger.error('Failed to get a list of running youtube-dl processes.');
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
return {
|
resolve({
|
||||||
details: err,
|
details: err,
|
||||||
success: false
|
success: false
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// processes that contain the string 'youtube-dl' in the name will be looped
|
// processes that contain the string 'youtube-dl' in the name will be looped
|
||||||
@@ -523,16 +524,18 @@ async function killAllDownloads() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
resolve({
|
||||||
return {
|
|
||||||
success: true
|
success: true
|
||||||
};
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setPortItemFromENV() {
|
async function setPortItemFromENV() {
|
||||||
|
return new Promise(resolve => {
|
||||||
config_api.setConfigItem('ytdl_port', backendPort.toString());
|
config_api.setConfigItem('ytdl_port', backendPort.toString());
|
||||||
await wait(100);
|
setTimeout(() => resolve(true), 100);
|
||||||
return true;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setAndLoadConfig() {
|
async function setAndLoadConfig() {
|
||||||
@@ -541,23 +544,27 @@ async function setAndLoadConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setConfigFromEnv() {
|
async function setConfigFromEnv() {
|
||||||
|
return new Promise(resolve => {
|
||||||
let config_items = getEnvConfigItems();
|
let config_items = getEnvConfigItems();
|
||||||
let success = config_api.setConfigItems(config_items);
|
let success = config_api.setConfigItems(config_items);
|
||||||
if (success) {
|
if (success) {
|
||||||
logger.info('Config items set using ENV variables.');
|
logger.info('Config items set using ENV variables.');
|
||||||
await wait(100);
|
setTimeout(() => resolve(true), 100);
|
||||||
return true;
|
|
||||||
} else {
|
} else {
|
||||||
logger.error('ERROR: Failed to set config items using ENV variables.');
|
logger.error('ERROR: Failed to set config items using ENV variables.');
|
||||||
return false;
|
resolve(false);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
|
return new Promise(async resolve => {
|
||||||
loadConfigValues();
|
loadConfigValues();
|
||||||
|
|
||||||
// creates archive path if missing
|
// creates archive path if missing
|
||||||
await fs.ensureDir(archivePath);
|
if (!fs.existsSync(archivePath)){
|
||||||
|
fs.mkdirSync(archivePath);
|
||||||
|
}
|
||||||
|
|
||||||
// get subscriptions
|
// get subscriptions
|
||||||
if (allowSubscriptions) {
|
if (allowSubscriptions) {
|
||||||
@@ -579,7 +586,9 @@ async function loadConfig() {
|
|||||||
// start the server here
|
// start the server here
|
||||||
startServer();
|
startServer();
|
||||||
|
|
||||||
return true;
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadConfigValues() {
|
function loadConfigValues() {
|
||||||
@@ -696,17 +705,17 @@ function generateEnvVarConfigItem(key) {
|
|||||||
return {key: key, value: process['env'][key]};
|
return {key: key, value: process['env'][key]};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMp3s() {
|
function getMp3s() {
|
||||||
let mp3s = [];
|
let mp3s = [];
|
||||||
var files = await utils.recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
|
var files = utils.recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
let file = files[i];
|
let file = files[i];
|
||||||
var file_path = file.substring(audioFolderPath.length, file.length);
|
var file_path = file.substring(audioFolderPath.length, file.length);
|
||||||
|
|
||||||
var stats = await fs.stat(file);
|
var stats = fs.statSync(file);
|
||||||
|
|
||||||
var id = file_path.substring(0, file_path.length-4);
|
var id = file_path.substring(0, file_path.length-4);
|
||||||
var jsonobj = await utils.getJSONMp3(id, audioFolderPath);
|
var jsonobj = utils.getJSONMp3(id, audioFolderPath);
|
||||||
if (!jsonobj) continue;
|
if (!jsonobj) continue;
|
||||||
var title = jsonobj.title;
|
var title = jsonobj.title;
|
||||||
var url = jsonobj.webpage_url;
|
var url = jsonobj.webpage_url;
|
||||||
@@ -725,9 +734,9 @@ async function getMp3s() {
|
|||||||
return mp3s;
|
return mp3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMp4s(relative_path = true) {
|
function getMp4s(relative_path = true) {
|
||||||
let mp4s = [];
|
let mp4s = [];
|
||||||
var files = await utils.recFindByExt(videoFolderPath, 'mp4');
|
var files = utils.recFindByExt(videoFolderPath, 'mp4');
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
let file = files[i];
|
let file = files[i];
|
||||||
var file_path = file.substring(videoFolderPath.length, file.length);
|
var file_path = file.substring(videoFolderPath.length, file.length);
|
||||||
@@ -735,7 +744,7 @@ async function getMp4s(relative_path = true) {
|
|||||||
var stats = fs.statSync(file);
|
var stats = fs.statSync(file);
|
||||||
|
|
||||||
var id = file_path.substring(0, file_path.length-4);
|
var id = file_path.substring(0, file_path.length-4);
|
||||||
var jsonobj = await utils.getJSONMp4(id, videoFolderPath);
|
var jsonobj = utils.getJSONMp4(id, videoFolderPath);
|
||||||
if (!jsonobj) continue;
|
if (!jsonobj) continue;
|
||||||
var title = jsonobj.title;
|
var title = jsonobj.title;
|
||||||
var url = jsonobj.webpage_url;
|
var url = jsonobj.webpage_url;
|
||||||
@@ -840,12 +849,12 @@ function getVideoFormatID(name)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvided = null, user_uid = null) {
|
async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvided = null) {
|
||||||
|
return new Promise(async resolve => {
|
||||||
let zipFolderPath = null;
|
let zipFolderPath = null;
|
||||||
|
|
||||||
if (!fullPathProvided) {
|
if (!fullPathProvided) {
|
||||||
zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath);
|
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 {
|
} else {
|
||||||
zipFolderPath = path.join(__dirname, config_api.getConfigItem('ytdl_subscriptions_base_path'));
|
zipFolderPath = path.join(__dirname, config_api.getConfigItem('ytdl_subscriptions_base_path'));
|
||||||
}
|
}
|
||||||
@@ -870,18 +879,24 @@ async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvid
|
|||||||
for (let i = 0; i < fileNames.length; i++) {
|
for (let i = 0; i < fileNames.length; i++) {
|
||||||
let fileName = fileNames[i];
|
let fileName = fileNames[i];
|
||||||
let fileNamePathRemoved = path.parse(fileName).base;
|
let fileNamePathRemoved = path.parse(fileName).base;
|
||||||
let file_path = !fullPathProvided ? path.join(zipFolderPath, fileName + ext) : fileName;
|
let file_path = !fullPathProvided ? zipFolderPath + fileName + ext : fileName;
|
||||||
archive.file(file_path, {name: fileNamePathRemoved + ext})
|
archive.file(file_path, {name: fileNamePathRemoved + ext})
|
||||||
}
|
}
|
||||||
|
|
||||||
await archive.finalize();
|
await archive.finalize();
|
||||||
|
|
||||||
// wait a tiny bit for the zip to reload in fs
|
// wait a tiny bit for the zip to reload in fs
|
||||||
await wait(100);
|
setTimeout(function() {
|
||||||
return path.join(zipFolderPath,outputName + '.zip');
|
resolve(path.join(zipFolderPath,outputName + '.zip'));
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAudioFile(name, customPath = null, blacklistMode = false) {
|
async function deleteAudioFile(name, customPath = null, blacklistMode = false) {
|
||||||
|
return new Promise(resolve => {
|
||||||
let filePath = customPath ? customPath : audioFolderPath;
|
let filePath = customPath ? customPath : audioFolderPath;
|
||||||
|
|
||||||
var jsonPath = path.join(filePath,name+'.mp3.info.json');
|
var jsonPath = path.join(filePath,name+'.mp3.info.json');
|
||||||
@@ -894,24 +909,24 @@ async function deleteAudioFile(name, customPath = null, blacklistMode = false) {
|
|||||||
altJSONPath = path.join(__dirname, altJSONPath);
|
altJSONPath = path.join(__dirname, altJSONPath);
|
||||||
audioFilePath = path.join(__dirname, audioFilePath);
|
audioFilePath = path.join(__dirname, audioFilePath);
|
||||||
|
|
||||||
let jsonExists = await fs.pathExists(jsonPath);
|
let jsonExists = fs.existsSync(jsonPath);
|
||||||
let thumbnailExists = await fs.pathExists(thumbnailPath);
|
let thumbnailExists = fs.existsSync(thumbnailPath);
|
||||||
|
|
||||||
if (!jsonExists) {
|
if (!jsonExists) {
|
||||||
if (await fs.pathExists(altJSONPath)) {
|
if (fs.existsSync(altJSONPath)) {
|
||||||
jsonExists = true;
|
jsonExists = true;
|
||||||
jsonPath = altJSONPath;
|
jsonPath = altJSONPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!thumbnailExists) {
|
if (!thumbnailExists) {
|
||||||
if (await fs.pathExists(altThumbnailPath)) {
|
if (fs.existsSync(altThumbnailPath)) {
|
||||||
thumbnailExists = true;
|
thumbnailExists = true;
|
||||||
thumbnailPath = altThumbnailPath;
|
thumbnailPath = altThumbnailPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let audioFileExists = await fs.pathExists(audioFilePath);
|
let audioFileExists = fs.existsSync(audioFilePath);
|
||||||
|
|
||||||
if (config_api.descriptors[name]) {
|
if (config_api.descriptors[name]) {
|
||||||
try {
|
try {
|
||||||
@@ -929,36 +944,40 @@ async function deleteAudioFile(name, customPath = null, blacklistMode = false) {
|
|||||||
|
|
||||||
// get ID from JSON
|
// get ID from JSON
|
||||||
|
|
||||||
var jsonobj = await utils.getJSONMp3(name, filePath);
|
var jsonobj = utils.getJSONMp3(name, filePath);
|
||||||
let id = null;
|
let id = null;
|
||||||
if (jsonobj) id = jsonobj.id;
|
if (jsonobj) id = jsonobj.id;
|
||||||
|
|
||||||
// use subscriptions API to remove video from the archive file, and write it to the blacklist
|
// use subscriptions API to remove video from the archive file, and write it to the blacklist
|
||||||
if (await fs.pathExists(archive_path)) {
|
if (fs.existsSync(archive_path)) {
|
||||||
const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null;
|
const line = id ? subscriptions_api.removeIDFromArchive(archive_path, id) : null;
|
||||||
if (blacklistMode && line) await writeToBlacklist('audio', line);
|
if (blacklistMode && line) writeToBlacklist('audio', line);
|
||||||
} else {
|
} else {
|
||||||
logger.info('Could not find archive file for audio files. Creating...');
|
logger.info('Could not find archive file for audio files. Creating...');
|
||||||
await fs.close(await fs.open(archive_path, 'w'));
|
fs.closeSync(fs.openSync(archive_path, 'w'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonExists) await fs.unlink(jsonPath);
|
if (jsonExists) fs.unlinkSync(jsonPath);
|
||||||
if (thumbnailExists) await fs.unlink(thumbnailPath);
|
if (thumbnailExists) fs.unlinkSync(thumbnailPath);
|
||||||
if (audioFileExists) {
|
if (audioFileExists) {
|
||||||
await fs.unlink(audioFilePath);
|
fs.unlink(audioFilePath, function(err) {
|
||||||
if (await fs.pathExists(jsonPath) || await fs.pathExists(audioFilePath)) {
|
if (fs.existsSync(jsonPath) || fs.existsSync(audioFilePath)) {
|
||||||
return false;
|
resolve(false);
|
||||||
} else {
|
} else {
|
||||||
return true;
|
resolve(true);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// TODO: tell user that the file didn't exist
|
// TODO: tell user that the file didn't exist
|
||||||
return true;
|
resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
|
async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
|
||||||
|
return new Promise(resolve => {
|
||||||
let filePath = customPath ? customPath : videoFolderPath;
|
let filePath = customPath ? customPath : videoFolderPath;
|
||||||
var jsonPath = path.join(filePath,name+'.info.json');
|
var jsonPath = path.join(filePath,name+'.info.json');
|
||||||
|
|
||||||
@@ -970,19 +989,19 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
|
|||||||
jsonPath = path.join(__dirname, jsonPath);
|
jsonPath = path.join(__dirname, jsonPath);
|
||||||
videoFilePath = path.join(__dirname, videoFilePath);
|
videoFilePath = path.join(__dirname, videoFilePath);
|
||||||
|
|
||||||
let jsonExists = await fs.pathExists(jsonPath);
|
let jsonExists = fs.existsSync(jsonPath);
|
||||||
let videoFileExists = await fs.pathExists(videoFilePath);
|
let videoFileExists = fs.existsSync(videoFilePath);
|
||||||
let thumbnailExists = await fs.pathExists(thumbnailPath);
|
let thumbnailExists = fs.existsSync(thumbnailPath);
|
||||||
|
|
||||||
if (!jsonExists) {
|
if (!jsonExists) {
|
||||||
if (await fs.pathExists(altJSONPath)) {
|
if (fs.existsSync(altJSONPath)) {
|
||||||
jsonExists = true;
|
jsonExists = true;
|
||||||
jsonPath = altJSONPath;
|
jsonPath = altJSONPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!thumbnailExists) {
|
if (!thumbnailExists) {
|
||||||
if (await fs.pathExists(altThumbnailPath)) {
|
if (fs.existsSync(altThumbnailPath)) {
|
||||||
thumbnailExists = true;
|
thumbnailExists = true;
|
||||||
thumbnailPath = altThumbnailPath;
|
thumbnailPath = altThumbnailPath;
|
||||||
}
|
}
|
||||||
@@ -1004,67 +1023,86 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
|
|||||||
|
|
||||||
// get ID from JSON
|
// get ID from JSON
|
||||||
|
|
||||||
var jsonobj = await utils.getJSONMp4(name, filePath);
|
var jsonobj = utils.getJSONMp4(name, filePath);
|
||||||
let id = null;
|
let id = null;
|
||||||
if (jsonobj) id = jsonobj.id;
|
if (jsonobj) id = jsonobj.id;
|
||||||
|
|
||||||
// use subscriptions API to remove video from the archive file, and write it to the blacklist
|
// use subscriptions API to remove video from the archive file, and write it to the blacklist
|
||||||
if (await fs.pathExists(archive_path)) {
|
if (fs.existsSync(archive_path)) {
|
||||||
const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null;
|
const line = id ? subscriptions_api.removeIDFromArchive(archive_path, id) : null;
|
||||||
if (blacklistMode && line) await writeToBlacklist('video', line);
|
if (blacklistMode && line) writeToBlacklist('video', line);
|
||||||
} else {
|
} else {
|
||||||
logger.info('Could not find archive file for videos. Creating...');
|
logger.info('Could not find archive file for videos. Creating...');
|
||||||
fs.closeSync(fs.openSync(archive_path, 'w'));
|
fs.closeSync(fs.openSync(archive_path, 'w'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonExists) await fs.unlink(jsonPath);
|
if (jsonExists) fs.unlinkSync(jsonPath);
|
||||||
if (thumbnailExists) await fs.unlink(thumbnailPath);
|
if (thumbnailExists) fs.unlinkSync(thumbnailPath);
|
||||||
if (videoFileExists) {
|
if (videoFileExists) {
|
||||||
await fs.unlink(videoFilePath);
|
fs.unlink(videoFilePath, function(err) {
|
||||||
if (await fs.pathExists(jsonPath) || await fs.pathExists(videoFilePath)) {
|
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
|
||||||
return false;
|
resolve(false);
|
||||||
} else {
|
} else {
|
||||||
return true;
|
resolve(true);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// TODO: tell user that the file didn't exist
|
// TODO: tell user that the file didn't exist
|
||||||
return true;
|
resolve(true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
});
|
||||||
* @param {'audio' | 'video'} type
|
|
||||||
* @param {string[]} fileNames
|
|
||||||
*/
|
|
||||||
async function getAudioOrVideoInfos(type, fileNames) {
|
|
||||||
let result = await Promise.all(fileNames.map(async fileName => {
|
|
||||||
let fileLocation = videoFolderPath+fileName;
|
|
||||||
if (type === 'audio') {
|
|
||||||
fileLocation += '.mp3.info.json';
|
|
||||||
} else if (type === 'video') {
|
|
||||||
fileLocation += '.info.json';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await fs.pathExists(fileLocation)) {
|
// replaces .webm with appropriate extension
|
||||||
let data = await fs.readFile(fileLocation);
|
function getTrueFileName(unfixed_path, type) {
|
||||||
|
let fixed_path = unfixed_path;
|
||||||
|
|
||||||
|
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
||||||
|
let unfixed_parts = unfixed_path.split('.');
|
||||||
|
const old_ext = unfixed_parts[unfixed_parts.length-1];
|
||||||
|
|
||||||
|
|
||||||
|
if (old_ext !== new_ext) {
|
||||||
|
unfixed_parts[unfixed_parts.length-1] = new_ext;
|
||||||
|
fixed_path = unfixed_parts.join('.');
|
||||||
|
}
|
||||||
|
return fixed_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAudioInfos(fileNames) {
|
||||||
|
let result = [];
|
||||||
|
for (let i = 0; i < fileNames.length; i++) {
|
||||||
|
let fileName = fileNames[i];
|
||||||
|
let fileLocation = audioFolderPath+fileName+'.mp3.info.json';
|
||||||
|
if (fs.existsSync(fileLocation)) {
|
||||||
|
let data = fs.readFileSync(fileLocation);
|
||||||
try {
|
try {
|
||||||
return JSON.parse(data);
|
result.push(JSON.parse(data));
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
let suffix;
|
logger.error(`Could not find info for file ${fileName}.mp3`);
|
||||||
if (type === 'audio') {
|
}
|
||||||
suffix += '.mp3';
|
}
|
||||||
} else if (type === 'video') {
|
}
|
||||||
suffix += '.mp4';
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(`Could not find info for file ${fileName}${suffix}`);
|
function getVideoInfos(fileNames) {
|
||||||
|
let result = [];
|
||||||
|
for (let i = 0; i < fileNames.length; i++) {
|
||||||
|
let fileName = fileNames[i];
|
||||||
|
let fileLocation = videoFolderPath+fileName+'.info.json';
|
||||||
|
if (fs.existsSync(fileLocation)) {
|
||||||
|
let data = fs.readFileSync(fileLocation);
|
||||||
|
try {
|
||||||
|
result.push(JSON.parse(data));
|
||||||
|
} catch(e) {
|
||||||
|
logger.error(`Could not find info for file ${fileName}.mp4`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
}));
|
return result;
|
||||||
|
|
||||||
return result.filter(data => data != null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloads
|
// downloads
|
||||||
@@ -1121,7 +1159,12 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// store info in download for future use
|
// store info in download for future use
|
||||||
|
if (Array.isArray(info)) {
|
||||||
|
download['fileNames'] = [];
|
||||||
|
for (let info_obj of info) download['fileNames'].push(info_obj['_filename']);
|
||||||
|
} else {
|
||||||
download['_filename'] = info['_filename'];
|
download['_filename'] = info['_filename'];
|
||||||
|
}
|
||||||
download['filesize'] = utils.getExpectedFileSize(info);
|
download['filesize'] = utils.getExpectedFileSize(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1362,6 +1405,7 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateArgs(url, type, options) {
|
async function generateArgs(url, type, options) {
|
||||||
|
return new Promise(async resolve => {
|
||||||
var videopath = '%(title)s';
|
var videopath = '%(title)s';
|
||||||
var globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
var globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
||||||
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||||
@@ -1423,7 +1467,7 @@ async function generateArgs(url, type, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (useCookies) {
|
if (useCookies) {
|
||||||
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
||||||
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
||||||
} else {
|
} 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.');
|
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
|
||||||
@@ -1439,26 +1483,26 @@ async function generateArgs(url, type, options) {
|
|||||||
const archive_folder = options.user ? path.join(fileFolderPath, 'archives') : archivePath;
|
const archive_folder = options.user ? path.join(fileFolderPath, 'archives') : archivePath;
|
||||||
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
|
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
|
||||||
|
|
||||||
await fs.ensureDir(archive_folder);
|
fs.ensureDirSync(archive_folder);
|
||||||
|
|
||||||
// create archive file if it doesn't exist
|
// create archive file if it doesn't exist
|
||||||
if (!(await fs.pathExists(archive_path))) {
|
if (!fs.existsSync(archive_path)) {
|
||||||
await fs.close(await fs.open(archive_path, 'w'));
|
fs.closeSync(fs.openSync(archive_path, 'w'));
|
||||||
}
|
}
|
||||||
|
|
||||||
let blacklist_path = options.user ? path.join(fileFolderPath, 'archives', `blacklist_${type}.txt`) : path.join(archivePath, `blacklist_${type}.txt`);
|
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
|
// create blacklist file if it doesn't exist
|
||||||
if (!(await fs.pathExists(blacklist_path))) {
|
if (!fs.existsSync(blacklist_path)) {
|
||||||
await fs.close(await fs.open(blacklist_path, 'w'));
|
fs.closeSync(fs.openSync(blacklist_path, 'w'));
|
||||||
}
|
}
|
||||||
|
|
||||||
let merged_path = path.join(fileFolderPath, `merged_${type}.txt`);
|
let merged_path = path.join(fileFolderPath, `merged_${type}.txt`);
|
||||||
await fs.ensureFile(merged_path);
|
fs.ensureFileSync(merged_path);
|
||||||
// merges blacklist and regular archive
|
// merges blacklist and regular archive
|
||||||
let inputPathList = [archive_path, blacklist_path];
|
let inputPathList = [archive_path, blacklist_path];
|
||||||
let status = await mergeFiles(inputPathList, merged_path);
|
let status = await mergeFiles(inputPathList, merged_path);
|
||||||
|
|
||||||
options.merged_string = await fs.readFile(merged_path, "utf8");
|
options.merged_string = fs.readFileSync(merged_path, "utf8");
|
||||||
|
|
||||||
downloadConfig.push('--download-archive', merged_path);
|
downloadConfig.push('--download-archive', merged_path);
|
||||||
}
|
}
|
||||||
@@ -1479,7 +1523,8 @@ async function generateArgs(url, type, options) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
|
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
|
||||||
return downloadConfig;
|
resolve(downloadConfig);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideoInfoByURL(url, args = [], download = null) {
|
async function getVideoInfoByURL(url, args = [], download = null) {
|
||||||
@@ -1552,11 +1597,11 @@ async function convertFileToMp3(input_file, output_file) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeToBlacklist(type, line) {
|
function writeToBlacklist(type, line) {
|
||||||
let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
|
let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
|
||||||
// adds newline to the beginning of the line
|
// adds newline to the beginning of the line
|
||||||
line = '\n' + line;
|
line = '\n' + line;
|
||||||
await fs.appendFile(blacklistPath, line);
|
fs.appendFileSync(blacklistPath, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
// download management functions
|
// download management functions
|
||||||
@@ -1573,12 +1618,15 @@ function checkDownloadPercent(download) {
|
|||||||
Any file that starts with <video title> will be counted as part of the "bytes downloaded", which will
|
Any file that starts with <video title> will be counted as part of the "bytes downloaded", which will
|
||||||
be divided by the "total expected bytes."
|
be divided by the "total expected bytes."
|
||||||
*/
|
*/
|
||||||
const file_id = download['file_id'];
|
// assume it's a playlist for logic reasons
|
||||||
const filename = path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4)));
|
const fileNames = Array.isArray(download['fileNames']) ? download['fileNames']
|
||||||
const resulting_file_size = download['filesize'];
|
: [path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4)))];
|
||||||
|
|
||||||
glob(`${filename}*`, (err, files) => {
|
|
||||||
|
const resulting_file_size = download['filesize'];
|
||||||
let sum_size = 0;
|
let sum_size = 0;
|
||||||
|
let glob_str = '';
|
||||||
|
glob(`{${fileNames.join(',')}, }*`, (err, files) => {
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
try {
|
try {
|
||||||
const file_stats = fs.statSync(file);
|
const file_stats = fs.statSync(file);
|
||||||
@@ -1700,6 +1748,21 @@ function removeFileExtension(filename) {
|
|||||||
return filename_parts.join('.');
|
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) {
|
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-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||||
res.header("Access-Control-Allow-Origin", getOrigin());
|
res.header("Access-Control-Allow-Origin", getOrigin());
|
||||||
@@ -1715,12 +1778,8 @@ app.use(function(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
} else if (req.query.apiKey === admin_token) {
|
} else if (req.query.apiKey === admin_token) {
|
||||||
next();
|
next();
|
||||||
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key')) {
|
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
||||||
if (req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
|
||||||
next();
|
next();
|
||||||
} else {
|
|
||||||
res.status(401).send('Invalid API key');
|
|
||||||
}
|
|
||||||
} else if (req.path.includes('/api/video/') || req.path.includes('/api/audio/')) {
|
} else if (req.path.includes('/api/video/') || req.path.includes('/api/audio/')) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
@@ -1742,9 +1801,9 @@ const optionalJwt = function (req, res, next) {
|
|||||||
const uuid = using_body ? req.body.uuid : req.query.uuid;
|
const uuid = using_body ? req.body.uuid : req.query.uuid;
|
||||||
const uid = using_body ? req.body.uid : req.query.uid;
|
const uid = using_body ? req.body.uid : req.query.uid;
|
||||||
const type = using_body ? req.body.type : req.query.type;
|
const type = using_body ? req.body.type : req.query.type;
|
||||||
const playlist_id = using_body ? req.body.id : req.query.id;
|
const file = !req.query.id ? auth_api.getUserVideo(uuid, uid, type, true, req.body) : auth_api.getUserPlaylist(uuid, req.query.id, null, true);
|
||||||
const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, type, true, req.body) : auth_api.getUserPlaylist(uuid, playlist_id, null, false);
|
const is_shared = file ? file['sharingEnabled'] : false;
|
||||||
if (file) {
|
if (is_shared) {
|
||||||
req.can_watch = true;
|
req.can_watch = true;
|
||||||
return next();
|
return next();
|
||||||
} else {
|
} else {
|
||||||
@@ -1848,21 +1907,8 @@ app.post('/api/killAllDownloads', optionalJwt, async function(req, res) {
|
|||||||
res.send(result_obj);
|
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
|
// gets all download mp3s
|
||||||
app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
app.get('/api/getMp3s', optionalJwt, function(req, res) {
|
||||||
var mp3s = db.get('files.audio').value(); // getMp3s();
|
var mp3s = db.get('files.audio').value(); // getMp3s();
|
||||||
var playlists = db.get('playlists.audio').value();
|
var playlists = db.get('playlists.audio').value();
|
||||||
const is_authenticated = req.isAuthenticated();
|
const is_authenticated = req.isAuthenticated();
|
||||||
@@ -1876,7 +1922,10 @@ app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
|||||||
mp3s = JSON.parse(JSON.stringify(mp3s));
|
mp3s = JSON.parse(JSON.stringify(mp3s));
|
||||||
|
|
||||||
// add thumbnails if present
|
// add thumbnails if present
|
||||||
await addThumbnails(mp3s);
|
mp3s.forEach(mp3 => {
|
||||||
|
if (mp3['thumbnailPath'] && fs.existsSync(mp3['thumbnailPath']))
|
||||||
|
mp3['thumbnailBlob'] = fs.readFileSync(mp3['thumbnailPath']);
|
||||||
|
});
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
mp3s: mp3s,
|
mp3s: mp3s,
|
||||||
@@ -1885,7 +1934,7 @@ app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// gets all download mp4s
|
// gets all download mp4s
|
||||||
app.get('/api/getMp4s', optionalJwt, async function(req, res) {
|
app.get('/api/getMp4s', optionalJwt, function(req, res) {
|
||||||
var mp4s = db.get('files.video').value(); // getMp4s();
|
var mp4s = db.get('files.video').value(); // getMp4s();
|
||||||
var playlists = db.get('playlists.video').value();
|
var playlists = db.get('playlists.video').value();
|
||||||
|
|
||||||
@@ -1900,7 +1949,10 @@ app.get('/api/getMp4s', optionalJwt, async function(req, res) {
|
|||||||
mp4s = JSON.parse(JSON.stringify(mp4s));
|
mp4s = JSON.parse(JSON.stringify(mp4s));
|
||||||
|
|
||||||
// add thumbnails if present
|
// add thumbnails if present
|
||||||
await addThumbnails(mp4s);
|
mp4s.forEach(mp4 => {
|
||||||
|
if (mp4['thumbnailPath'] && fs.existsSync(mp4['thumbnailPath']))
|
||||||
|
mp4['thumbnailBlob'] = fs.readFileSync(mp4['thumbnailPath']);
|
||||||
|
});
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
mp4s: mp4s,
|
mp4s: mp4s,
|
||||||
@@ -1946,7 +1998,7 @@ app.post('/api/getFile', optionalJwt, function (req, res) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
app.post('/api/getAllFiles', optionalJwt, function (req, res) {
|
||||||
// these are returned
|
// these are returned
|
||||||
let files = [];
|
let files = [];
|
||||||
let playlists = [];
|
let playlists = [];
|
||||||
@@ -1956,7 +2008,7 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
|||||||
let audios = null;
|
let audios = null;
|
||||||
let audio_playlists = null;
|
let audio_playlists = null;
|
||||||
let video_playlists = null;
|
let video_playlists = null;
|
||||||
let subscriptions = config_api.getConfigItem('ytdl_allow_subscriptions') ? (subscriptions_api.getAllSubscriptions(req.isAuthenticated() ? req.user.uid : null)) : [];
|
let subscriptions = subscriptions_api.getAllSubscriptions(req.isAuthenticated() ? req.user.uid : null);
|
||||||
|
|
||||||
// get basic info depending on multi-user mode being enabled
|
// get basic info depending on multi-user mode being enabled
|
||||||
if (req.isAuthenticated()) {
|
if (req.isAuthenticated()) {
|
||||||
@@ -1989,7 +2041,10 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
|||||||
files = JSON.parse(JSON.stringify(files));
|
files = JSON.parse(JSON.stringify(files));
|
||||||
|
|
||||||
// add thumbnails if present
|
// add thumbnails if present
|
||||||
await addThumbnails(files);
|
files.forEach(file => {
|
||||||
|
if (file['thumbnailPath'] && fs.existsSync(file['thumbnailPath']))
|
||||||
|
file['thumbnailBlob'] = fs.readFileSync(file['thumbnailPath']);
|
||||||
|
});
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
files: files,
|
files: files,
|
||||||
@@ -2193,7 +2248,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
|||||||
let appended_base_path = path.join(base_path, (subscription.isPlaylist ? 'playlists' : 'channels'), subscription.name, '/');
|
let appended_base_path = path.join(base_path, (subscription.isPlaylist ? 'playlists' : 'channels'), subscription.name, '/');
|
||||||
let files;
|
let files;
|
||||||
try {
|
try {
|
||||||
files = await utils.recFindByExt(appended_base_path, 'mp4');
|
files = utils.recFindByExt(appended_base_path, 'mp4');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
files = null;
|
files = null;
|
||||||
logger.info('Failed to get folder for subscription: ' + subscription.name + ' at path ' + appended_base_path);
|
logger.info('Failed to get folder for subscription: ' + subscription.name + ' at path ' + appended_base_path);
|
||||||
@@ -2408,7 +2463,7 @@ app.post('/api/deleteMp3', optionalJwt, async (req, res) => {
|
|||||||
var blacklistMode = req.body.blacklistMode;
|
var blacklistMode = req.body.blacklistMode;
|
||||||
|
|
||||||
if (req.isAuthenticated()) {
|
if (req.isAuthenticated()) {
|
||||||
let success = await auth_api.deleteUserFile(req.user.uid, uid, 'audio', blacklistMode);
|
let success = auth_api.deleteUserFile(req.user.uid, uid, 'audio', blacklistMode);
|
||||||
res.send(success);
|
res.send(success);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2417,7 +2472,7 @@ app.post('/api/deleteMp3', optionalJwt, async (req, res) => {
|
|||||||
var name = audio_obj.id;
|
var name = audio_obj.id;
|
||||||
var fullpath = audioFolderPath + name + ".mp3";
|
var fullpath = audioFolderPath + name + ".mp3";
|
||||||
var wasDeleted = false;
|
var wasDeleted = false;
|
||||||
if (await fs.pathExists(fullpath))
|
if (fs.existsSync(fullpath))
|
||||||
{
|
{
|
||||||
deleteAudioFile(name, null, blacklistMode);
|
deleteAudioFile(name, null, blacklistMode);
|
||||||
db.get('files.audio').remove({uid: uid}).write();
|
db.get('files.audio').remove({uid: uid}).write();
|
||||||
@@ -2439,7 +2494,7 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
|
|||||||
var blacklistMode = req.body.blacklistMode;
|
var blacklistMode = req.body.blacklistMode;
|
||||||
|
|
||||||
if (req.isAuthenticated()) {
|
if (req.isAuthenticated()) {
|
||||||
let success = await auth_api.deleteUserFile(req.user.uid, uid, 'video', blacklistMode);
|
let success = auth_api.deleteUserFile(req.user.uid, uid, 'video', blacklistMode);
|
||||||
res.send(success);
|
res.send(success);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2448,7 +2503,7 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
|
|||||||
var name = video_obj.id;
|
var name = video_obj.id;
|
||||||
var fullpath = videoFolderPath + name + ".mp4";
|
var fullpath = videoFolderPath + name + ".mp4";
|
||||||
var wasDeleted = false;
|
var wasDeleted = false;
|
||||||
if (await fs.pathExists(fullpath))
|
if (fs.existsSync(fullpath))
|
||||||
{
|
{
|
||||||
wasDeleted = await deleteVideoFile(name, null, blacklistMode);
|
wasDeleted = await deleteVideoFile(name, null, blacklistMode);
|
||||||
db.get('files.video').remove({uid: uid}).write();
|
db.get('files.video').remove({uid: uid}).write();
|
||||||
@@ -2482,9 +2537,9 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => {
|
|||||||
let base_path = fileFolderPath;
|
let base_path = fileFolderPath;
|
||||||
let usersFileFolder = null;
|
let usersFileFolder = null;
|
||||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||||
if (multiUserMode && (req.body.uuid || req.user.uid)) {
|
if (multiUserMode && req.body.uuid) {
|
||||||
usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
base_path = path.join(usersFileFolder, req.body.uuid ? req.body.uuid : req.user.uid, type);
|
base_path = path.join(usersFileFolder, req.body.uuid, type);
|
||||||
}
|
}
|
||||||
if (!subscriptionName) {
|
if (!subscriptionName) {
|
||||||
file = path.join(__dirname, base_path, fileNames + ext);
|
file = path.join(__dirname, base_path, fileNames + ext);
|
||||||
@@ -2501,8 +2556,7 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => {
|
|||||||
for (let i = 0; i < fileNames.length; i++) {
|
for (let i = 0; i < fileNames.length; i++) {
|
||||||
fileNames[i] = decodeURIComponent(fileNames[i]);
|
fileNames[i] = decodeURIComponent(fileNames[i]);
|
||||||
}
|
}
|
||||||
file = await createPlaylistZipFile(fileNames, type, outputName, fullPathProvided, req.body.uuid);
|
file = await createPlaylistZipFile(fileNames, type, outputName, fullPathProvided);
|
||||||
if (!path.isAbsolute(file)) file = path.join(__dirname, file);
|
|
||||||
}
|
}
|
||||||
res.sendFile(file, function (err) {
|
res.sendFile(file, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -2534,7 +2588,7 @@ app.post('/api/downloadArchive', async (req, res) => {
|
|||||||
|
|
||||||
let full_archive_path = path.join(archive_dir, 'archive.txt');
|
let full_archive_path = path.join(archive_dir, 'archive.txt');
|
||||||
|
|
||||||
if (await fs.pathExists(full_archive_path)) {
|
if (fs.existsSync(full_archive_path)) {
|
||||||
res.sendFile(full_archive_path);
|
res.sendFile(full_archive_path);
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(404);
|
res.sendStatus(404);
|
||||||
@@ -2546,14 +2600,14 @@ var upload_multer = multer({ dest: __dirname + '/appdata/' });
|
|||||||
app.post('/api/uploadCookies', upload_multer.single('cookies'), async (req, res) => {
|
app.post('/api/uploadCookies', upload_multer.single('cookies'), async (req, res) => {
|
||||||
const new_path = path.join(__dirname, 'appdata', 'cookies.txt');
|
const new_path = path.join(__dirname, 'appdata', 'cookies.txt');
|
||||||
|
|
||||||
if (await fs.pathExists(req.file.path)) {
|
if (fs.existsSync(req.file.path)) {
|
||||||
await fs.rename(req.file.path, new_path);
|
fs.renameSync(req.file.path, new_path);
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(500);
|
res.sendStatus(500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await fs.pathExists(new_path)) {
|
if (fs.existsSync(new_path)) {
|
||||||
res.send({success: true});
|
res.send({success: true});
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(500);
|
res.sendStatus(500);
|
||||||
@@ -2776,9 +2830,9 @@ app.post('/api/logs', async function(req, res) {
|
|||||||
let logs = null;
|
let logs = null;
|
||||||
let lines = req.body.lines;
|
let lines = req.body.lines;
|
||||||
logs_path = path.join('appdata', 'logs', 'combined.log')
|
logs_path = path.join('appdata', 'logs', 'combined.log')
|
||||||
if (await fs.pathExists(logs_path)) {
|
if (fs.existsSync(logs_path)) {
|
||||||
if (lines) logs = await read_last_lines.read(logs_path, lines);
|
if (lines) logs = await read_last_lines.read(logs_path, lines);
|
||||||
else logs = await fs.readFile(logs_path, 'utf8');
|
else logs = fs.readFileSync(logs_path, 'utf8');
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
logger.error(`Failed to find logs file at the expected location: ${logs_path}`)
|
logger.error(`Failed to find logs file at the expected location: ${logs_path}`)
|
||||||
@@ -2794,10 +2848,8 @@ app.post('/api/clearAllLogs', async function(req, res) {
|
|||||||
logs_err_path = path.join('appdata', 'logs', 'error.log');
|
logs_err_path = path.join('appdata', 'logs', 'error.log');
|
||||||
let success = false;
|
let success = false;
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
fs.writeFileSync(logs_path, '');
|
||||||
fs.writeFile(logs_path, ''),
|
fs.writeFileSync(logs_err_path, '');
|
||||||
fs.writeFile(logs_err_path, '')
|
|
||||||
])
|
|
||||||
success = true;
|
success = true;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
@@ -2814,8 +2866,10 @@ app.post('/api/clearAllLogs', async function(req, res) {
|
|||||||
let type = req.body.type;
|
let type = req.body.type;
|
||||||
let result = null;
|
let result = null;
|
||||||
if (!urlMode) {
|
if (!urlMode) {
|
||||||
if (type === 'audio' || type === 'video') {
|
if (type === 'audio') {
|
||||||
result = await getAudioOrVideoInfos(type, fileNames);
|
result = getAudioInfos(fileNames)
|
||||||
|
} else if (type === 'video') {
|
||||||
|
result = getVideoInfos(fileNames);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result = await getUrlInfos(fileNames);
|
result = await getUrlInfos(fileNames);
|
||||||
@@ -2888,7 +2942,7 @@ app.post('/api/deleteUser', optionalJwt, async (req, res) => {
|
|||||||
const user_db_obj = users_db.get('users').find({uid: uid});
|
const user_db_obj = users_db.get('users').find({uid: uid});
|
||||||
if (user_db_obj.value()) {
|
if (user_db_obj.value()) {
|
||||||
// user exists, let's delete
|
// user exists, let's delete
|
||||||
await fs.remove(user_folder);
|
deleteFolderRecursive(user_folder);
|
||||||
users_db.get('users').remove({uid: uid}).write();
|
users_db.get('users').remove({uid: uid}).write();
|
||||||
}
|
}
|
||||||
res.send({success: true});
|
res.send({success: true});
|
||||||
|
|||||||
@@ -139,12 +139,12 @@ exports.registerUser = function(req, res) {
|
|||||||
exports.passport.use(new LocalStrategy({
|
exports.passport.use(new LocalStrategy({
|
||||||
usernameField: 'username',
|
usernameField: 'username',
|
||||||
passwordField: 'password'},
|
passwordField: 'password'},
|
||||||
async function(username, password, done) {
|
function(username, password, done) {
|
||||||
const user = users_db.get('users').find({name: username}).value();
|
const user = users_db.get('users').find({name: username}).value();
|
||||||
if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
|
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.auth_method && user.auth_method !== 'internal') { return done(null, false); }
|
||||||
if (user) {
|
if (user) {
|
||||||
return done(null, (await bcrypt.compare(password, user.passhash)) ? user : false);
|
return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
@@ -226,13 +226,15 @@ exports.ensureAuthenticatedElseError = function(req, res, next) {
|
|||||||
|
|
||||||
// change password
|
// change password
|
||||||
exports.changeUserPassword = async function(user_uid, new_pass) {
|
exports.changeUserPassword = async function(user_uid, new_pass) {
|
||||||
try {
|
return new Promise(resolve => {
|
||||||
const hash = await bcrypt.hash(new_pass, saltRounds);
|
bcrypt.hash(new_pass, saltRounds)
|
||||||
|
.then(function(hash) {
|
||||||
users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
|
users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
|
||||||
return true;
|
resolve(true);
|
||||||
} catch (err) {
|
}).catch(err => {
|
||||||
return false;
|
resolve(false);
|
||||||
}
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// change user permissions
|
// change user permissions
|
||||||
@@ -281,7 +283,6 @@ exports.getUserVideos = function(user_uid, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
|
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
|
||||||
let file = null;
|
|
||||||
if (!type) {
|
if (!type) {
|
||||||
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
|
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@@ -295,7 +296,7 @@ exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false
|
|||||||
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
||||||
|
|
||||||
// prevent unauthorized users from accessing the file info
|
// prevent unauthorized users from accessing the file info
|
||||||
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
if (requireSharing && !file['sharingEnabled']) file = null;
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
@@ -350,7 +351,7 @@ exports.registerUserFile = function(user_uid, file_object, type) {
|
|||||||
.write();
|
.write();
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.deleteUserFile = async function(user_uid, file_uid, type, blacklistMode = false) {
|
exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) {
|
||||||
let success = false;
|
let success = false;
|
||||||
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
||||||
if (file_obj) {
|
if (file_obj) {
|
||||||
@@ -373,20 +374,20 @@ exports.deleteUserFile = async function(user_uid, file_uid, type, blacklistMode
|
|||||||
.remove({
|
.remove({
|
||||||
uid: file_uid
|
uid: file_uid
|
||||||
}).write();
|
}).write();
|
||||||
if (await fs.pathExists(full_path)) {
|
if (fs.existsSync(full_path)) {
|
||||||
// remove json and file
|
// remove json and file
|
||||||
const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json');
|
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');
|
const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json');
|
||||||
let youtube_id = null;
|
let youtube_id = null;
|
||||||
if (await fs.pathExists(json_path)) {
|
if (fs.existsSync(json_path)) {
|
||||||
youtube_id = await fs.readJSON(json_path).id;
|
youtube_id = fs.readJSONSync(json_path).id;
|
||||||
await fs.unlink(json_path);
|
fs.unlinkSync(json_path);
|
||||||
} else if (await fs.pathExists(alternate_json_path)) {
|
} else if (fs.existsSync(alternate_json_path)) {
|
||||||
youtube_id = await fs.readJSON(alternate_json_path).id;
|
youtube_id = fs.readJSONSync(alternate_json_path).id;
|
||||||
await fs.unlink(alternate_json_path);
|
fs.unlinkSync(alternate_json_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.unlink(full_path);
|
fs.unlinkSync(full_path);
|
||||||
|
|
||||||
// do archive stuff
|
// do archive stuff
|
||||||
|
|
||||||
@@ -395,17 +396,17 @@ exports.deleteUserFile = async function(user_uid, file_uid, type, blacklistMode
|
|||||||
const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`);
|
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
|
// use subscriptions API to remove video from the archive file, and write it to the blacklist
|
||||||
if (await fs.pathExists(archive_path)) {
|
if (fs.existsSync(archive_path)) {
|
||||||
const line = youtube_id ? await subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
|
const line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
|
||||||
if (blacklistMode && line) {
|
if (blacklistMode && line) {
|
||||||
let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`);
|
let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`);
|
||||||
// adds newline to the beginning of the line
|
// adds newline to the beginning of the line
|
||||||
line = '\n' + line;
|
line = '\n' + line;
|
||||||
await fs.appendFile(blacklistPath, line);
|
fs.appendFileSync(blacklistPath, line);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info(`Could not find archive file for ${type} files. Creating...`);
|
logger.info(`Could not find archive file for ${type} files. Creating...`);
|
||||||
await fs.ensureFile(archive_path);
|
fs.ensureFileSync(archive_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,9 +182,9 @@ async function importUnregisteredFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// run through check list and check each file to see if it's missing from the db
|
// run through check list and check each file to see if it's missing from the db
|
||||||
for (const dir_to_check of dirs_to_check) {
|
dirs_to_check.forEach(dir_to_check => {
|
||||||
// recursively get all files in dir's path
|
// recursively get all files in dir's path
|
||||||
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
|
const files = utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
|
||||||
|
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
// check if file exists in db, if not add it
|
// check if file exists in db, if not add it
|
||||||
@@ -195,7 +195,7 @@ async function importUnregisteredFiles() {
|
|||||||
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,18 +79,17 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
|||||||
else
|
else
|
||||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
// get videos
|
// get videos
|
||||||
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
|
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
|
||||||
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||||
if (useCookies) {
|
if (useCookies) {
|
||||||
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
||||||
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
||||||
} else {
|
} 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.');
|
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) {
|
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
logger.info('Subscribe: got info for subscription ' + sub.id);
|
logger.info('Subscribe: got info for subscription ' + sub.id);
|
||||||
@@ -153,6 +152,7 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function unsubscribe(sub, deleteMode, user_uid = null) {
|
async function unsubscribe(sub, deleteMode, user_uid = null) {
|
||||||
|
return new Promise(async resolve => {
|
||||||
let basePath = null;
|
let basePath = null;
|
||||||
if (user_uid)
|
if (user_uid)
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
@@ -172,17 +172,19 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
if (deleteMode && (await fs.pathExists(appendedBasePath))) {
|
if (deleteMode && fs.existsSync(appendedBasePath)) {
|
||||||
if (sub.archive && (await fs.pathExists(sub.archive))) {
|
if (sub.archive && fs.existsSync(sub.archive)) {
|
||||||
const archive_file_path = path.join(sub.archive, 'archive.txt');
|
const archive_file_path = path.join(sub.archive, 'archive.txt');
|
||||||
// deletes archive if it exists
|
// deletes archive if it exists
|
||||||
if (await fs.pathExists(archive_file_path)) {
|
if (fs.existsSync(archive_file_path)) {
|
||||||
await fs.unlink(archive_file_path);
|
fs.unlinkSync(archive_file_path);
|
||||||
}
|
}
|
||||||
await fs.rmdir(sub.archive);
|
fs.rmdirSync(sub.archive);
|
||||||
}
|
}
|
||||||
await fs.remove(appendedBasePath);
|
deleteFolderRecursive(appendedBasePath);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
|
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
|
||||||
@@ -200,7 +202,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
|||||||
const name = file;
|
const name = file;
|
||||||
let retrievedID = null;
|
let retrievedID = null;
|
||||||
sub_db.get('videos').remove({uid: file_uid}).write();
|
sub_db.get('videos').remove({uid: file_uid}).write();
|
||||||
|
return new Promise(resolve => {
|
||||||
let filePath = appendedBasePath;
|
let filePath = appendedBasePath;
|
||||||
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
|
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
|
||||||
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
|
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
|
||||||
@@ -208,50 +210,53 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
|||||||
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
||||||
var altImageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
var altImageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
||||||
|
|
||||||
const [jsonExists, videoFileExists, imageFileExists, altImageFileExists] = await Promise.all([
|
jsonExists = fs.existsSync(jsonPath);
|
||||||
fs.pathExists(jsonPath),
|
videoFileExists = fs.existsSync(videoFilePath);
|
||||||
fs.pathExists(videoFilePath),
|
imageFileExists = fs.existsSync(imageFilePath);
|
||||||
fs.pathExists(imageFilePath),
|
altImageFileExists = fs.existsSync(altImageFilePath);
|
||||||
fs.pathExists(altImageFilePath),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (jsonExists) {
|
if (jsonExists) {
|
||||||
retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id'];
|
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
|
||||||
await fs.unlink(jsonPath);
|
fs.unlinkSync(jsonPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageFileExists) {
|
if (imageFileExists) {
|
||||||
await fs.unlink(imageFilePath);
|
fs.unlinkSync(imageFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (altImageFileExists) {
|
if (altImageFileExists) {
|
||||||
await fs.unlink(altImageFilePath);
|
fs.unlinkSync(altImageFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoFileExists) {
|
if (videoFileExists) {
|
||||||
await fs.unlink(videoFilePath);
|
fs.unlink(videoFilePath, function(err) {
|
||||||
if ((await fs.pathExists(jsonPath)) || (await fs.pathExists(videoFilePath))) {
|
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
|
||||||
return false;
|
resolve(false);
|
||||||
} else {
|
} else {
|
||||||
// check if the user wants the video to be redownloaded (deleteForever === false)
|
// check if the user wants the video to be redownloaded (deleteForever === false)
|
||||||
if (!deleteForever && useArchive && sub.archive && retrievedID) {
|
if (!deleteForever && useArchive && sub.archive && retrievedID) {
|
||||||
const archive_path = path.join(sub.archive, 'archive.txt')
|
const archive_path = path.join(sub.archive, 'archive.txt')
|
||||||
// if archive exists, remove line with video ID
|
// if archive exists, remove line with video ID
|
||||||
if (await fs.pathExists(archive_path)) {
|
if (fs.existsSync(archive_path)) {
|
||||||
await removeIDFromArchive(archive_path, retrievedID);
|
removeIDFromArchive(archive_path, retrievedID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
resolve(true);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// TODO: tell user that the file didn't exist
|
// TODO: tell user that the file didn't exist
|
||||||
return true;
|
resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideosForSub(sub, user_uid = null) {
|
async function getVideosForSub(sub, user_uid = null) {
|
||||||
|
return new Promise(resolve => {
|
||||||
if (!subExists(sub.id, user_uid)) {
|
if (!subExists(sub.id, user_uid)) {
|
||||||
return false;
|
resolve(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get sub_db
|
// get sub_db
|
||||||
@@ -283,9 +288,9 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
|
|
||||||
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`;
|
let fullOutput = appendedBasePath + '/%(title)s' + ext;
|
||||||
if (sub.custom_output) {
|
if (sub.custom_output) {
|
||||||
fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`;
|
fullOutput = appendedBasePath + '/' + sub.custom_output + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
|
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
|
||||||
@@ -333,7 +338,7 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
|
|
||||||
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||||
if (useCookies) {
|
if (useCookies) {
|
||||||
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
||||||
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
||||||
} else {
|
} 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.');
|
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
|
||||||
@@ -346,8 +351,6 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
|
|
||||||
// get videos
|
// get videos
|
||||||
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||||
if (err && !output) {
|
if (err && !output) {
|
||||||
@@ -453,8 +456,23 @@ function getAppendedBasePath(sub, base_path) {
|
|||||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeIDFromArchive(archive_path, id) {
|
// https://stackoverflow.com/a/32197381/8088021
|
||||||
let data = await fs.readFile(archive_path, {encoding: 'utf-8'});
|
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'});
|
||||||
if (!data) {
|
if (!data) {
|
||||||
logger.error('Archive could not be found.');
|
logger.error('Archive could not be found.');
|
||||||
return;
|
return;
|
||||||
@@ -475,7 +493,7 @@ async function removeIDFromArchive(archive_path, id) {
|
|||||||
|
|
||||||
// UPDATE FILE WITH NEW DATA
|
// UPDATE FILE WITH NEW DATA
|
||||||
const updatedData = dataArray.join('\n');
|
const updatedData = dataArray.join('\n');
|
||||||
await fs.writeFile(archive_path, updatedData);
|
fs.writeFileSync(archive_path, updatedData);
|
||||||
if (line) return line;
|
if (line) return line;
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ const config_api = require('./config');
|
|||||||
|
|
||||||
const is_windows = process.platform === 'win32';
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
// replaces .webm with appropriate extension
|
|
||||||
function getTrueFileName(unfixed_path, type) {
|
function getTrueFileName(unfixed_path, type) {
|
||||||
let fixed_path = unfixed_path;
|
let fixed_path = unfixed_path;
|
||||||
|
|
||||||
@@ -20,21 +19,21 @@ function getTrueFileName(unfixed_path, type) {
|
|||||||
return fixed_path;
|
return fixed_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDownloadedFilesByType(basePath, type) {
|
function getDownloadedFilesByType(basePath, type) {
|
||||||
// return empty array if the path doesn't exist
|
// return empty array if the path doesn't exist
|
||||||
if (!(await fs.pathExists(basePath))) return [];
|
if (!fs.existsSync(basePath)) return [];
|
||||||
|
|
||||||
let files = [];
|
let files = [];
|
||||||
const ext = type === 'audio' ? 'mp3' : 'mp4';
|
const ext = type === 'audio' ? 'mp3' : 'mp4';
|
||||||
var located_files = await recFindByExt(basePath, ext);
|
var located_files = recFindByExt(basePath, ext);
|
||||||
for (let i = 0; i < located_files.length; i++) {
|
for (let i = 0; i < located_files.length; i++) {
|
||||||
let file = located_files[i];
|
let file = located_files[i];
|
||||||
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
|
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
|
||||||
|
|
||||||
var stats = await fs.stat(file);
|
var stats = fs.statSync(file);
|
||||||
|
|
||||||
var id = file_path.substring(0, file_path.length-4);
|
var id = file_path.substring(0, file_path.length-4);
|
||||||
var jsonobj = await getJSONByType(type, id, basePath);
|
var jsonobj = getJSONByType(type, id, basePath);
|
||||||
if (!jsonobj) continue;
|
if (!jsonobj) continue;
|
||||||
var title = jsonobj.title;
|
var title = jsonobj.title;
|
||||||
var url = jsonobj.webpage_url;
|
var url = jsonobj.webpage_url;
|
||||||
@@ -106,20 +105,28 @@ function getDownloadedThumbnail(name, type, customPath = null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExpectedFileSize(info_json) {
|
function getExpectedFileSize(input_info_jsons) {
|
||||||
if (info_json['filesize']) {
|
// treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner
|
||||||
return info_json['filesize'];
|
const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons];
|
||||||
}
|
|
||||||
|
|
||||||
const formats = info_json['format_id'].split('+');
|
|
||||||
let expected_filesize = 0;
|
let expected_filesize = 0;
|
||||||
|
|
||||||
|
info_jsons.forEach(info_json => {
|
||||||
|
if (info_json['filesize']) {
|
||||||
|
expected_filesize += info_json['filesize'];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formats = info_json['format_id'].split('+');
|
||||||
|
let individual_expected_filesize = 0;
|
||||||
formats.forEach(format_id => {
|
formats.forEach(format_id => {
|
||||||
info_json.formats.forEach(available_format => {
|
info_json.formats.forEach(available_format => {
|
||||||
if (available_format.format_id === format_id && available_format.filesize) {
|
if (available_format.format_id === format_id && available_format.filesize) {
|
||||||
expected_filesize += available_format.filesize;
|
individual_expected_filesize += available_format.filesize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
expected_filesize += individual_expected_filesize;
|
||||||
|
});
|
||||||
|
|
||||||
return expected_filesize;
|
return expected_filesize;
|
||||||
}
|
}
|
||||||
@@ -159,16 +166,17 @@ function deleteJSONFile(name, type, customPath = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function recFindByExt(base,ext,files,result)
|
function recFindByExt(base,ext,files,result)
|
||||||
{
|
{
|
||||||
files = files || (await fs.readdir(base))
|
files = files || fs.readdirSync(base)
|
||||||
result = result || []
|
result = result || []
|
||||||
|
|
||||||
for (const file of files) {
|
files.forEach(
|
||||||
|
function (file) {
|
||||||
var newbase = path.join(base,file)
|
var newbase = path.join(base,file)
|
||||||
if ( (await fs.stat(newbase)).isDirectory() )
|
if ( fs.statSync(newbase).isDirectory() )
|
||||||
{
|
{
|
||||||
result = await recFindByExt(newbase,ext,await fs.readdir(newbase),result)
|
result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -178,6 +186,7 @@ async function recFindByExt(base,ext,files,result)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
||||||
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
|
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar>{{subscription.name}}</a>
|
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
|
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export class CustomPlaylistsComponent implements OnInit {
|
|||||||
const playlist = args.file;
|
const playlist = args.file;
|
||||||
const index = args.index;
|
const index = args.index;
|
||||||
const playlistID = playlist.id;
|
const playlistID = playlist.id;
|
||||||
this.postsService.removePlaylist(playlistID, playlist.type).subscribe(res => {
|
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
|
||||||
if (res['success']) {
|
if (res['success']) {
|
||||||
this.playlists.splice(index, 1);
|
this.playlists.splice(index, 1);
|
||||||
this.postsService.openSnackBar('Playlist successfully removed.', '');
|
this.postsService.openSnackBar('Playlist successfully removed.', '');
|
||||||
|
|||||||
@@ -32,12 +32,12 @@
|
|||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<ng-container *ngIf="normal_files_received">
|
<ng-container *ngIf="normal_files_received">
|
||||||
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
|
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
||||||
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
|
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
<div (mouseover)="elevated=true" (mouseout)="elevated=false" (contextmenu)="onRightClick($event)" style="position: relative; width: fit-content;">
|
<div (mouseover)="elevated=true" (mouseout)="elevated=false" style="position: relative; width: fit-content;">
|
||||||
<div *ngIf="!loading" class="download-time"><mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon> {{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</div>
|
<div *ngIf="!loading" class="download-time"><mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon> {{file_obj.registered | date:'shortDate'}}</div>
|
||||||
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
|
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
|
||||||
<!-- The context menu trigger must be kept above the "more info" menu -->
|
|
||||||
<div style="visibility: hidden; position: fixed"
|
|
||||||
[style.left]="contextMenuPosition.x"
|
|
||||||
[style.top]="contextMenuPosition.y"
|
|
||||||
[matMenuTriggerFor]="context_menu">
|
|
||||||
</div>
|
|
||||||
<button [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
<button [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||||
<mat-menu #context_menu>
|
|
||||||
<ng-container *ngIf="!loading">
|
|
||||||
<button (click)="navigateToFile($event)" mat-menu-item><mat-icon>open_in_browser</mat-icon><ng-container i18n="Open file button">Open file</ng-container></button>
|
|
||||||
<button (click)="navigateToFile({ctrlKey: true})" mat-menu-item><mat-icon>open_in_new</mat-icon><ng-container i18n="Open file in new tab">Open file in new tab</ng-container></button>
|
|
||||||
</ng-container>
|
|
||||||
</mat-menu>
|
|
||||||
<mat-menu #action_menu="matMenu">
|
<mat-menu #action_menu="matMenu">
|
||||||
<ng-container *ngIf="!is_playlist && !loading">
|
<ng-container *ngIf="!is_playlist && !loading">
|
||||||
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
||||||
|
|||||||
@@ -1,22 +1,7 @@
|
|||||||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from '@angular/core';
|
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
||||||
import { DomSanitizer } from '@angular/platform-browser';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
import { MatMenuTrigger } from '@angular/material/menu';
|
|
||||||
import { registerLocaleData } from '@angular/common';
|
|
||||||
import localeGB from '@angular/common/locales/en-GB';
|
|
||||||
import localeFR from '@angular/common/locales/fr';
|
|
||||||
import localeES from '@angular/common/locales/es';
|
|
||||||
import localeDE from '@angular/common/locales/de';
|
|
||||||
import localeZH from '@angular/common/locales/zh';
|
|
||||||
import localeNB from '@angular/common/locales/nb';
|
|
||||||
|
|
||||||
registerLocaleData(localeGB);
|
|
||||||
registerLocaleData(localeFR);
|
|
||||||
registerLocaleData(localeES);
|
|
||||||
registerLocaleData(localeDE);
|
|
||||||
registerLocaleData(localeZH);
|
|
||||||
registerLocaleData(localeNB);
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-unified-file-card',
|
selector: 'app-unified-file-card',
|
||||||
@@ -43,16 +28,11 @@ export class UnifiedFileCardComponent implements OnInit {
|
|||||||
@Input() use_youtubedl_archive = false;
|
@Input() use_youtubedl_archive = false;
|
||||||
@Input() is_playlist = false;
|
@Input() is_playlist = false;
|
||||||
@Input() index: number;
|
@Input() index: number;
|
||||||
@Input() locale = null;
|
|
||||||
@Output() goToFile = new EventEmitter<any>();
|
@Output() goToFile = new EventEmitter<any>();
|
||||||
@Output() goToSubscription = new EventEmitter<any>();
|
@Output() goToSubscription = new EventEmitter<any>();
|
||||||
@Output() deleteFile = new EventEmitter<any>();
|
@Output() deleteFile = new EventEmitter<any>();
|
||||||
@Output() editPlaylist = new EventEmitter<any>();
|
@Output() editPlaylist = new EventEmitter<any>();
|
||||||
|
|
||||||
|
|
||||||
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
|
|
||||||
contextMenuPosition = { x: '0px', y: '0px' };
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Planned sizes:
|
Planned sizes:
|
||||||
small: 150x175
|
small: 150x175
|
||||||
@@ -107,15 +87,6 @@ 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) {
|
function fancyTimeFormat(time) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</ngx-file-drop>
|
</ngx-file-drop>
|
||||||
<div style="margin-top: 15px;">
|
<div style="margin-top: 15px;">
|
||||||
<p style="font-size: 14px;" i18n="Cookies upload warning">NOTE: Uploading new cookies will override your previous cookies. Also note that cookies are instance-wide, not per-user.</p>
|
<p style="font-size: 14px;" i18n="Cookies upload warning">NOTE: Uploading new cookies will overrride your previous cookies. Also note that cookies are instance-wide, not per-user.</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h4 mat-dialog-title i18n="Edit subscription dialog title prefix">Editing</h4> {{sub.name}}
|
<h4 mat-dialog-title i18n="Edit subscription dialog title">Editing {{sub.name}}</h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</mat-button-toggle-group>
|
</mat-button-toggle-group>
|
||||||
|
|
||||||
<div class="add-content-button">
|
<div class="add-content-button">
|
||||||
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu"><ng-container i18n="Add more content">Add more content</ng-container></button>
|
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu">Add more content</button>
|
||||||
</div>
|
</div>
|
||||||
<mat-menu #menu="matMenu">
|
<mat-menu #menu="matMenu">
|
||||||
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file}}</button>
|
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file}}</button>
|
||||||
@@ -24,5 +24,5 @@
|
|||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<!-- Save -->
|
<!-- Save -->
|
||||||
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent"><ng-container i18n="Save">Save</ng-container></button>
|
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent">Save</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -215,7 +215,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
// adds user token if in multi-user-mode
|
// adds user token if in multi-user-mode
|
||||||
const uuid_str = this.uuid ? `&uuid=${this.uuid}` : '';
|
const uuid_str = this.uuid ? `&uuid=${this.uuid}` : '';
|
||||||
const uid_str = (this.id || !this.db_file) ? '' : `&uid=${this.db_file.uid}`;
|
const uid_str = (this.id || !this.db_file) ? '' : `&uid=${this.db_file.uid}`;
|
||||||
const type_str = (this.id || !this.db_file || !this.db_file.type) ? '' : `&type=${this.db_file.type}`
|
const type_str = (this.id || !this.db_file) ? '' : `&type=${this.db_file.type}`
|
||||||
const id_str = this.id ? `&id=${this.id}` : '';
|
const id_str = this.id ? `&id=${this.id}` : '';
|
||||||
if (this.postsService.isLoggedIn) {
|
if (this.postsService.isLoggedIn) {
|
||||||
fullLocation += (this.subscriptionName ? '&' : '?') + `jwt=${this.postsService.token}`;
|
fullLocation += (this.subscriptionName ? '&' : '?') + `jwt=${this.postsService.token}`;
|
||||||
@@ -317,8 +317,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
|
const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
|
||||||
this.downloading = true;
|
this.downloading = true;
|
||||||
this.postsService.downloadFileFromServer(fileNames, this.type, zipName, null, null, null, null,
|
this.postsService.downloadFileFromServer(fileNames, this.type, zipName).subscribe(res => {
|
||||||
!this.uuid ? this.postsService.user.uid : this.uuid, this.id).subscribe(res => {
|
|
||||||
this.downloading = false;
|
this.downloading = false;
|
||||||
const blob: Blob = res;
|
const blob: Blob = res;
|
||||||
saveAs(blob, zipName + '.zip');
|
saveAs(blob, zipName + '.zip');
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { BehaviorSubject } from 'rxjs';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import * as Fingerprint2 from 'fingerprintjs2';
|
import * as Fingerprint2 from 'fingerprintjs2';
|
||||||
import { isoLangs } from './settings/locales_list';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostsService implements CanActivate {
|
export class PostsService implements CanActivate {
|
||||||
@@ -54,7 +53,6 @@ export class PostsService implements CanActivate {
|
|||||||
config = null;
|
config = null;
|
||||||
subscriptions = null;
|
subscriptions = null;
|
||||||
sidenav = null;
|
sidenav = null;
|
||||||
locale = isoLangs['en'];
|
|
||||||
|
|
||||||
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
|
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
|
||||||
public snackBar: MatSnackBar) {
|
public snackBar: MatSnackBar) {
|
||||||
@@ -116,17 +114,6 @@ export class PostsService implements CanActivate {
|
|||||||
if (localStorage.getItem('card_size')) {
|
if (localStorage.getItem('card_size')) {
|
||||||
this.card_size = 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<boolean> {
|
canActivate(route, state): Promise<boolean> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
@@ -236,7 +223,7 @@ export class PostsService implements CanActivate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
|
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
|
||||||
uid = null, uuid = null, id = null) {
|
uid = null, uuid = null) {
|
||||||
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
|
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
|
||||||
type: type,
|
type: type,
|
||||||
zip_mode: Array.isArray(fileName),
|
zip_mode: Array.isArray(fileName),
|
||||||
@@ -245,8 +232,7 @@ export class PostsService implements CanActivate {
|
|||||||
subscriptionName: subscriptionName,
|
subscriptionName: subscriptionName,
|
||||||
subPlaylist: subPlaylist,
|
subPlaylist: subPlaylist,
|
||||||
uuid: uuid,
|
uuid: uuid,
|
||||||
uid: uid,
|
uid: uid
|
||||||
id: id
|
|
||||||
},
|
},
|
||||||
{responseType: 'blob', params: this.httpOptions.params});
|
{responseType: 'blob', params: this.httpOptions.params});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,8 +122,7 @@ export const isoLangs = {
|
|||||||
},
|
},
|
||||||
'zh': {
|
'zh': {
|
||||||
'name': 'Chinese',
|
'name': 'Chinese',
|
||||||
'nativeName': '中文 (Zhōngwén), 汉语, 漢語',
|
'nativeName': '中文 (Zhōngwén), 汉语, 漢語'
|
||||||
'ngID': 'zh'
|
|
||||||
},
|
},
|
||||||
'cv': {
|
'cv': {
|
||||||
'name': 'Chuvash',
|
'name': 'Chuvash',
|
||||||
@@ -163,13 +162,7 @@ export const isoLangs = {
|
|||||||
},
|
},
|
||||||
'en': {
|
'en': {
|
||||||
'name': 'English',
|
'name': 'English',
|
||||||
'nativeName': 'English',
|
'nativeName': 'English'
|
||||||
'ngID': 'en-US'
|
|
||||||
},
|
|
||||||
'en-GB': {
|
|
||||||
'name': 'British English',
|
|
||||||
'nativeName': 'British English',
|
|
||||||
'ngID': 'en-GB'
|
|
||||||
},
|
},
|
||||||
'eo': {
|
'eo': {
|
||||||
'name': 'Esperanto',
|
'name': 'Esperanto',
|
||||||
@@ -197,8 +190,7 @@ export const isoLangs = {
|
|||||||
},
|
},
|
||||||
'fr': {
|
'fr': {
|
||||||
'name': 'French',
|
'name': 'French',
|
||||||
'nativeName': 'français',
|
'nativeName': 'français, langue française'
|
||||||
'ngID': 'fr'
|
|
||||||
},
|
},
|
||||||
'ff': {
|
'ff': {
|
||||||
'name': 'Fula; Fulah; Pulaar; Pular',
|
'name': 'Fula; Fulah; Pulaar; Pular',
|
||||||
@@ -214,8 +206,7 @@ export const isoLangs = {
|
|||||||
},
|
},
|
||||||
'de': {
|
'de': {
|
||||||
'name': 'German',
|
'name': 'German',
|
||||||
'nativeName': 'Deutsch',
|
'nativeName': 'Deutsch'
|
||||||
'ngID': 'de'
|
|
||||||
},
|
},
|
||||||
'el': {
|
'el': {
|
||||||
'name': 'Greek, Modern',
|
'name': 'Greek, Modern',
|
||||||
@@ -447,8 +438,7 @@ export const isoLangs = {
|
|||||||
},
|
},
|
||||||
'nb': {
|
'nb': {
|
||||||
'name': 'Norwegian Bokmål',
|
'name': 'Norwegian Bokmål',
|
||||||
'nativeName': 'Norsk bokmål',
|
'nativeName': 'Norsk bokmål'
|
||||||
'ngID': 'nb'
|
|
||||||
},
|
},
|
||||||
'nd': {
|
'nd': {
|
||||||
'name': 'North Ndebele',
|
'name': 'North Ndebele',
|
||||||
@@ -604,8 +594,7 @@ export const isoLangs = {
|
|||||||
},
|
},
|
||||||
'es': {
|
'es': {
|
||||||
'name': 'Spanish; Castilian',
|
'name': 'Spanish; Castilian',
|
||||||
'nativeName': 'español',
|
'nativeName': 'español'
|
||||||
'ngID': 'es'
|
|
||||||
},
|
},
|
||||||
'su': {
|
'su': {
|
||||||
'name': 'Sundanese',
|
'name': 'Sundanese',
|
||||||
|
|||||||
@@ -253,7 +253,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-2 mb-1">
|
<div class="col-12 mt-2 mb-1">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label><ng-container i18n="Log Level label">Log Level</ng-container></mat-label>
|
<mat-label><ng-container i18n="Logger level select label">Select a logger level</ng-container></mat-label>
|
||||||
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['logger_level']">
|
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['logger_level']">
|
||||||
<mat-option value="debug">Debug</mat-option>
|
<mat-option value="debug">Debug</mat-option>
|
||||||
<mat-option value="verbose">Verbose</mat-option>
|
<mat-option value="verbose">Verbose</mat-option>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo
|
|||||||
})
|
})
|
||||||
export class SettingsComponent implements OnInit {
|
export class SettingsComponent implements OnInit {
|
||||||
all_locales = isoLangs;
|
all_locales = isoLangs;
|
||||||
supported_locales = ['en', 'es', 'de', 'fr', 'zh', 'nb', 'en-GB'];
|
supported_locales = ['en', 'es', 'de'];
|
||||||
initialLocale = localStorage.getItem('locale');
|
initialLocale = localStorage.getItem('locale');
|
||||||
|
|
||||||
initial_config = null;
|
initial_config = null;
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export class SubscriptionComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.router.navigate(['/player', {fileNames: name,
|
this.router.navigate(['/player', {fileNames: name,
|
||||||
type: this.subscription.type ? this.subscription.type : 'video', subscriptionName: this.subscription.name,
|
type: this.subscription.type ? this.subscription.type : 'video', subscriptionName: this.subscription.name,
|
||||||
subPlaylist: this.subscription.isPlaylist}]);
|
subPlaylist: this.subscription.isPlaylist, uuid: this.postsService.user ? this.postsService.user.uid : null}]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Wiedergabeliste erstellen",
|
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Playlist erstellen",
|
||||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Name",
|
"cff1428d10d59d14e45edec3c735a27b5482db59": "Name",
|
||||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiodateien",
|
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiodateien",
|
||||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Videos",
|
"a52dae09be10ca3a65da918533ced3d3f4992238": "Videos",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Passwort",
|
"c32ef07f8803a223a83ed17024b38e8d82292407": "Passwort",
|
||||||
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
|
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
|
||||||
"9779715ac05308973d8f1c8658b29b986e92450f": "Ihre Audiodateien befinden sich hier",
|
"9779715ac05308973d8f1c8658b29b986e92450f": "Ihre Audiodateien befinden sich hier",
|
||||||
"47546e45bbb476baaaad38244db444c427ddc502": "Wiedergabelisten",
|
"47546e45bbb476baaaad38244db444c427ddc502": "Playlisten",
|
||||||
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Keine Wiedergabelisten verfügbar. Erstellen Sie eine aus Ihren heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
|
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Keine Wiedergabelisten verfügbar. Erstellen Sie eine aus Ihren heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
|
||||||
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Video",
|
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Video",
|
||||||
"960582a8b9d7942716866ecfb7718309728f2916": "Ihre Videodateien befinden sich hier",
|
"960582a8b9d7942716866ecfb7718309728f2916": "Ihre Videodateien befinden sich hier",
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"46826331da1949bd6fb74624447057099c9d20cd": "Video Basispfad",
|
"46826331da1949bd6fb74624447057099c9d20cd": "Video Basispfad",
|
||||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Dateipfad für Video-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.",
|
"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: ,,",
|
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Globale benutzerdefinierte Argumente für Downloads auf der Startseite. Argumente werden durch zwei Kommata voneinander getrennt: ,,",
|
||||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Herunterlader",
|
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader",
|
||||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Titel der Kopfzeile",
|
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Titel der Kopfzeile",
|
||||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Dateimanager aktivieren",
|
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Dateimanager aktivieren",
|
||||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Download-Manager aktivieren",
|
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Download-Manager aktivieren",
|
||||||
@@ -117,14 +117,14 @@
|
|||||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Speichern",
|
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Speichern",
|
||||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Schließen} false {Abbrechen} other {Andere}}",
|
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Schließen} false {Abbrechen} other {Andere}}",
|
||||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Über YoutubeDL-Material",
|
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Über YoutubeDL-Material",
|
||||||
"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.",
|
"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.",
|
||||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "beinhaltet viele tolle Funktionen! API, Docker und Lokalisierung werden unter anderem unterstützt. Informieren Sie sich über alle unterstützten Funktionen auf Github.",
|
"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:",
|
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Installierte Version:",
|
||||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Suche nach Updates ...",
|
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Suche nach Updates ...",
|
||||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Aktualisierung verfügbar",
|
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Aktualisierung verfügbar",
|
||||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Sie können über das Einstellungsmenü aktualisieren.",
|
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Sie können über das Einstellungsmenü aktualisieren.",
|
||||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "Haben Sie einen Fehler gefunden oder einen Vorschlag?",
|
"b33536f59b94ec935a16bd6869d836895dc5300c": "Haben Sie einen Fehler gefunden oder einen Vorschlag?",
|
||||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "um ein Ticket zu öffnen!",
|
"e1f398f38ff1534303d4bb80bd6cece245f24016": "um ein Ticket zu öffnen.",
|
||||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Ihr Profil",
|
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Ihr Profil",
|
||||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
||||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Erstellt:",
|
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Erstellt:",
|
||||||
@@ -138,8 +138,8 @@
|
|||||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Über",
|
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Über",
|
||||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Startseite",
|
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Startseite",
|
||||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements",
|
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements",
|
||||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Heruntergeladene",
|
"822fab38216f64e8166d368b59fe756ca39d301b": "Downloads",
|
||||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Wiedergabeliste teilen",
|
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Playlist teilen",
|
||||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Video teilen",
|
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Video teilen",
|
||||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Audio teilen",
|
"1d540dcd271b316545d070f9d182c372d923aadd": "Audio teilen",
|
||||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Freigabe aktivieren",
|
"1f6d14a780a37a97899dc611881e6bc971268285": "Freigabe aktivieren",
|
||||||
@@ -152,8 +152,8 @@
|
|||||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Download Start:",
|
"77b0c73840665945b25bd128709aa64c8f017e1c": "Download Start:",
|
||||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Download Ende:",
|
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Download Ende:",
|
||||||
"ad127117f9471612f47d01eae09709da444a36a4": "Dateipfad(e):",
|
"ad127117f9471612f47d01eae09709da444a36a4": "Dateipfad(e):",
|
||||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Wiedergabeliste oder einen Kanal",
|
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Playlist oder einen Kanal",
|
||||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "URL der Wiedergabeliste oder des Kanales",
|
"93efc99ae087fc116de708ecd3ace86ca237cf30": "Playlist oder Kanal URL",
|
||||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Benutzerdefinierter Name",
|
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Benutzerdefinierter Name",
|
||||||
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Dies ist optional",
|
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Dies ist optional",
|
||||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle Uploads herunterladen",
|
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle Uploads herunterladen",
|
||||||
@@ -168,18 +168,18 @@
|
|||||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanäle",
|
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanäle",
|
||||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Name nicht verfügbar. Kanal wird abgerufen...",
|
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Name nicht verfügbar. Kanal wird abgerufen...",
|
||||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Sie haben keine Kanäle abonniert.",
|
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Sie haben keine Kanäle abonniert.",
|
||||||
"2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Wiedergabeliste wird abgerufen.",
|
"2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Playlist wird abgerufen...",
|
||||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Wiedergabeliste abonniert.",
|
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Playlisten abonniert.",
|
||||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Suchen",
|
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Suchen",
|
||||||
"2054791b822475aeaea95c0119113de3200f5e1c": "Länge:",
|
"2054791b822475aeaea95c0119113de3200f5e1c": "Länge:",
|
||||||
"94e01842dcee90531caa52e4147f70679bac87fe": "Löschen und erneut herunterladen",
|
"94e01842dcee90531caa52e4147f70679bac87fe": "Löschen und erneut herunterladen",
|
||||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent löschen",
|
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent löschen",
|
||||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Aktualisierungsprogramm",
|
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
|
||||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "Wählen Sie eine Version:",
|
"1372e61c5bd06100844bd43b98b016aabc468f62": "Wählen Sie eine Version:",
|
||||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrieren",
|
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrieren",
|
||||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sitzungs-ID:",
|
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sitzungs-ID:",
|
||||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(aktuell)",
|
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(aktuell)",
|
||||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Herunterladen-Ereignisse verfügbar!",
|
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Downloads verfügbar.",
|
||||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
|
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
|
||||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
|
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
|
||||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
|
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
|
||||||
@@ -194,32 +194,5 @@
|
|||||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle",
|
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle",
|
||||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Aktionen",
|
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Aktionen",
|
||||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Benutzer hinzufügen",
|
"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"
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -197,49 +197,19 @@
|
|||||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
|
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
|
||||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
|
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
|
||||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol",
|
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol",
|
||||||
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registros",
|
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modify playlist",
|
||||||
"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",
|
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editar",
|
||||||
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modificar lista de reproducción",
|
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Sube nuevas cookies",
|
||||||
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Tipo",
|
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrastrar y soltar",
|
||||||
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Vídeo",
|
"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.",
|
||||||
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio",
|
"d01715b75228878a773ae6d059acc639d4898a03": "Anulación de descarga segura",
|
||||||
"d02888c485d3aeab6de628508f4a00312a722894": "Mis videos",
|
"00e274c496b094a019f0679c3fab3945793f3335": "Seleccione un nivel de registrador",
|
||||||
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Ir a suscripción",
|
"431e5f3a0dde88768d1074baedd65266412b3f02": "Utilizar Cookies",
|
||||||
"5656a06f17c24b2d7eae9c221567b209743829a9": "Abrir archivo en nueva pestaña",
|
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Establecer Cookies",
|
||||||
"ccf5ea825526ac490974336cb5c24352886abc07": "Abrir archivo",
|
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registros",
|
||||||
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Borrar registros",
|
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Solo audio",
|
||||||
"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.",
|
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Estos se agregan después de los argumentos estándar.",
|
||||||
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Modo de solo audio",
|
"98b6ec9ec138186d663e64770267b67334353d63": "Salida de archivo personalizada",
|
||||||
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Un error ha ocurrido:",
|
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Los registros aparecerán aquí",
|
||||||
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "Un error ha ocurrido",
|
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Líneas:"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,228 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
{
|
|
||||||
"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": "取消所有下载"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user