Compare commits

..

52 Commits

Author SHA1 Message Date
Isaac Abadi
edc22cc47b File cards now use the locale to format dates 2020-10-23 01:38:44 -04:00
Isaac Abadi
8366089444 Added support for French, Chinese (Mandarin, simplified), and Norweigan. Updated German and Spanish translations
- Updated README to reflect new official translator
- XLIFFs to come later
2020-10-02 03:05:54 -04:00
Tzahi12345
44445f0b67 Added code analysis GH action 2020-10-01 14:46:04 -04:00
Isaac Abadi
7dcc38c26d Updated string in settings: "Select a logger level" -> "Log Level"
Made modify playlist component fully translatable

Fixed typo in cookies settings text
2020-09-30 04:52:43 -04:00
Isaac Abadi
79b4b993f8 Fixed bug in translation source file (2) 2020-09-30 04:41:15 -04:00
Isaac Abadi
37a19eabe6 Fixed bug in source translation file 2020-09-30 04:34:52 -04:00
Isaac Abadi
91b892b21a Updated source translation file 2020-09-30 04:19:04 -04:00
Tzahi12345
fb72dee26f Merge pull request #216 from NotWoods/await
Use async versions of filesystem methods
2020-09-29 17:41:19 -04:00
Tiger Oakes
3e4e7edd90 Oops.
for in -> for of
2020-09-29 14:32:28 -07:00
Isaac Abadi
b8280e8646 Updated spanish translation file (2) 2020-09-29 16:58:17 -04:00
Isaac Abadi
70ee071e57 Cleaned up spanish translation file 2020-09-29 14:41:38 -04:00
Tiger Oakes
e26ac82c66 Fix missing keywords 2020-09-29 08:53:36 -07:00
Isaac Abadi
cdd2f78998 Fixed bug that prevented video playlists from being deleted 2020-09-27 05:05:45 -04:00
Tiger Oakes
21eafeab22 Make utils.recFindByExt and utils.getDownloadedFilesByType async 2020-09-26 15:24:41 -07:00
Tiger Oakes
f535d18cb9 Use async methods in auth and subscriptions 2020-09-26 15:14:37 -07:00
Tiger Oakes
2c43ce3c47 Use async versions of filesystem methods 2020-09-26 14:57:23 -07:00
Isaac Abadi
3d2d4efb31 Added context menu on right click of the unified file cards, with options to open a file in the player or do so in a new tab 2020-09-26 03:00:26 -04:00
Isaac Abadi
10922fedd7 Fixed bugs that prevented subscription videos from being downloaded and non-users from accessing shared videos 2020-09-26 00:29:13 -04:00
Isaac Abadi
96cf1b87d1 Fixed bug in subscriptions that caused audio files to be downloaded as webm 2020-09-26 00:08:22 -04:00
Tzahi12345
6bed5851ed Merge pull request #220 from Tzahi12345/fix-playlist-downloading-bug
Fixed bug that preventing playlists from being downloaded a zip
2020-09-26 00:04:43 -04:00
Isaac Abadi
6717a59422 Fixed bug that preventing playlists from being downloaded a zip 2020-09-24 02:26:58 -04:00
Isaac Abadi
899633e124 Fixed bug that showed users their subscription videos after subscriptions were disabled 2020-09-20 23:13:56 -04:00
Isaac Abadi
8fdc231f08 Updated new home page UI to support file manager disabling and permissions
- file manager enabled state is now cached for faster loading
2020-09-18 11:22:45 -04:00
Isaac Abadi
ae8f7a2a33 Fixed bug that prevented playlists from being navigated to 2020-09-18 11:05:13 -04:00
Tzahi12345
d0782bb444 Update README.md
Updated API docs

Fixes #213
2020-09-18 00:46:42 -04:00
Isaac Abadi
49210abb49 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-09-17 03:15:29 -04:00
Isaac Abadi
851bfb81ba File cards are now properly centered 2020-09-17 03:12:09 -04:00
Isaac Abadi
35d0d439fa Control-clicking file cards will now open the player in a new tab 2020-09-17 03:11:52 -04:00
Tzahi12345
ded3ad6dfc Merge pull request #212 from Tzahi12345/dependabot/npm_and_yarn/backend/node-fetch-2.6.1
Bump node-fetch from 2.6.0 to 2.6.1 in /backend
2020-09-12 17:07:53 -04:00
dependabot[bot]
61daf26641 Bump node-fetch from 2.6.0 to 2.6.1 in /backend
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-12 20:55:44 +00:00
Tzahi12345
95e53b9549 Fixed bug where unix paths would improperly parsed while importing unregistered files 2020-09-07 16:06:25 -04:00
Tzahi12345
46ed0fe992 Fixed bug in import unregistered logic where files in subfolders could not be found 2020-09-07 00:39:27 -04:00
Isaac Abadi
082252ab1e Updated sidenav logic for "side" mode, where it will now autoclose in the player, be open everywhere else 2020-08-31 15:21:58 -04:00
Tzahi12345
5eccaa13e5 Merge pull request #206 from Tzahi12345/downloader-improvements
Downloader improvements - updated system and bug fixes
2020-08-31 15:17:40 -04:00
Isaac Abadi
71633950b2 Comments cleanup 2020-08-31 15:03:04 -04:00
Isaac Abadi
f31dad0215 JSON metadata files are no longer kept if the associated setting is not enabled 2020-08-30 05:56:25 -04:00
Isaac Abadi
7efbe40bb2 Added setting for including metadata/thumbnails in the UI 2020-08-30 05:55:50 -04:00
Isaac Abadi
5b768b5bda JSON blobs were accidentally inserted into DB, stringifying then parsing the video file object fixes this 2020-08-30 05:42:52 -04:00
Isaac Abadi
365cbc3ffa Mkv/webm formats are now included for quality select (will get merged into mp4 at the end) 2020-08-29 23:08:23 -04:00
Isaac Abadi
44647f3306 Download progress is now shown when downloads are 1% complete or more (it was 15% before) 2020-08-29 23:06:40 -04:00
Isaac Abadi
8a7409478a Added the ability to download videos at higher resolutions than the highest mp4 (fixes #76)
Deprecates normal downloading method. The "safe" method is now always used, and download progress is now estimated using the predicted end file size

Thumbnails are now auto downloaded along with the other metadata
2020-08-29 23:05:37 -04:00
Tzahi12345
70159813e5 Merge pull request #205 from Tzahi12345/add-ldap-auth
Added ability to register/login through LDAP
2020-08-26 04:30:43 -04:00
Tzahi12345
d292275956 Unfinished subscriptions will no longer cause an error during server startup 2020-08-24 05:13:27 -04:00
Tzahi12345
ba2acedb94 Files are now reloaded when you navigate back home 2020-08-24 05:13:01 -04:00
Tzahi12345
aa0558b770 Subscriptions are now reloaded on subscribe/unsubscribe in PostsService 2020-08-24 05:11:56 -04:00
Tzahi12345
d7f04fc90a Text for file duration in the unified file card component is now always black 2020-08-24 05:11:04 -04:00
Tzahi12345
087c9f1bb1 Added public directory to the gitignore 2020-08-24 02:44:52 -04:00
Tzahi12345
f874617965 Fixes bug where cached JWT token could prevent default admin creation 2020-08-24 02:44:39 -04:00
Tzahi12345
8fb8543829 Merge pull request #203 from Tzahi12345/arm-autobuild-test
Fix ARM autobuild
2020-08-24 02:20:19 -04:00
Tzahi12345
70d89d310c Removed unneeded hooks 2020-08-24 02:18:39 -04:00
Tzahi12345
c48aaaf13c Possible fix for arm autobuild (2) 2020-08-24 00:25:59 -04:00
Tzahi12345
6cf7ea193a Possible fix for arm autobuild 2020-08-24 00:21:10 -04:00
44 changed files with 2407 additions and 1823 deletions

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 12 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

1
.gitignore vendored
View File

@@ -65,3 +65,4 @@ backend/appdata/logs/error.log
backend/appdata/users.json
backend/users/*
backend/appdata/cookies.txt
backend/public

View File

@@ -90,7 +90,7 @@ environment:
## API
[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio)
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)
To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing.
@@ -111,6 +111,7 @@ If you're interested in translating the app into a new language, check out the [
Official translators:
* Spanish - tzahi12345
* German - UnlimitedCookies
* Chinese - TyRoyal
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.

View File

@@ -1,11 +1,15 @@
FROM arm32v7/alpine:3.12 as frontend
FROM alpine:3.12 as frontend
RUN apk add --no-cache \
npm
npm \
curl
RUN npm install -g @angular/cli
WORKDIR /build
RUN curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
COPY [ "package.json", "package-lock.json", "/build/" ]
RUN npm install
@@ -17,7 +21,7 @@ RUN ng build --prod
FROM arm32v7/alpine:3.12
COPY qemu-arm-static /usr/bin
COPY --from=frontend /build/qemu-arm-static /usr/bin
ENV UID=1000 \
GID=1000 \

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,9 @@
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false
"safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
},
"Extra": {
"title_top": "YoutubeDL-Material",

View File

@@ -139,12 +139,12 @@ exports.registerUser = function(req, res) {
exports.passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password'},
function(username, password, done) {
async function(username, password, done) {
const user = users_db.get('users').find({name: username}).value();
if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
if (user.auth_method && user.auth_method !== 'internal') { return done(null, false); }
if (user) {
return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
return done(null, (await bcrypt.compare(password, user.passhash)) ? user : false);
}
}
));
@@ -160,7 +160,7 @@ exports.passport.use(new LdapStrategy(getLDAPConfiguration,
// check if ldap auth is enabled
const ldap_enabled = config_api.getConfigItem('ytdl_auth_method') === 'ldap';
if (!ldap_enabled) return done(null, false);
const user_uid = user.uid;
let db_user = users_db.get('users').find({uid: user_uid}).value();
if (!db_user) {
@@ -226,15 +226,13 @@ exports.ensureAuthenticatedElseError = function(req, res, next) {
// change password
exports.changeUserPassword = async function(user_uid, new_pass) {
return new Promise(resolve => {
bcrypt.hash(new_pass, saltRounds)
.then(function(hash) {
users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
resolve(true);
}).catch(err => {
resolve(false);
});
});
try {
const hash = await bcrypt.hash(new_pass, saltRounds);
users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
return true;
} catch (err) {
return false;
}
}
// change user permissions
@@ -283,6 +281,7 @@ exports.getUserVideos = function(user_uid, type) {
}
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
let file = null;
if (!type) {
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
if (!file) {
@@ -296,7 +295,7 @@ exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
// prevent unauthorized users from accessing the file info
if (requireSharing && !file['sharingEnabled']) file = null;
if (file && !file['sharingEnabled'] && requireSharing) file = null;
return file;
}
@@ -351,7 +350,7 @@ exports.registerUserFile = function(user_uid, file_object, type) {
.write();
}
exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) {
exports.deleteUserFile = async function(user_uid, file_uid, type, blacklistMode = false) {
let success = false;
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
if (file_obj) {
@@ -374,20 +373,20 @@ exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = fals
.remove({
uid: file_uid
}).write();
if (fs.existsSync(full_path)) {
if (await fs.pathExists(full_path)) {
// remove json and file
const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json');
const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json');
let youtube_id = null;
if (fs.existsSync(json_path)) {
youtube_id = fs.readJSONSync(json_path).id;
fs.unlinkSync(json_path);
} else if (fs.existsSync(alternate_json_path)) {
youtube_id = fs.readJSONSync(alternate_json_path).id;
fs.unlinkSync(alternate_json_path);
if (await fs.pathExists(json_path)) {
youtube_id = await fs.readJSON(json_path).id;
await fs.unlink(json_path);
} else if (await fs.pathExists(alternate_json_path)) {
youtube_id = await fs.readJSON(alternate_json_path).id;
await fs.unlink(alternate_json_path);
}
fs.unlinkSync(full_path);
await fs.unlink(full_path);
// do archive stuff
@@ -396,17 +395,17 @@ exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = fals
const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`);
// use subscriptions API to remove video from the archive file, and write it to the blacklist
if (fs.existsSync(archive_path)) {
const line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
if (await fs.pathExists(archive_path)) {
const line = youtube_id ? await subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
if (blacklistMode && line) {
let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`);
// adds newline to the beginning of the line
line = '\n' + line;
fs.appendFileSync(blacklistPath, line);
await fs.appendFile(blacklistPath, line);
}
} else {
logger.info(`Could not find archive file for ${type} files. Creating...`);
fs.ensureFileSync(archive_path);
await fs.ensureFile(archive_path);
}
}
}
@@ -532,7 +531,7 @@ function generateUserObject(userid, username, hash, auth_method = 'internal') {
role: userid === 'admin' && auth_method === 'internal' ? 'admin' : 'user',
permissions: [],
permission_overrides: [],
auth_method: auth_method
auth_method: auth_method
};
return new_user;
}

View File

@@ -186,7 +186,9 @@ DEFAULT_CONFIG = {
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false
"safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
},
"Extra": {
"title_top": "YoutubeDL-Material",

View File

@@ -30,6 +30,14 @@ let CONFIG_ITEMS = {
'key': 'ytdl_safe_download_override',
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
},
'ytdl_include_thumbnail': {
'key': 'ytdl_include_thumbnail',
'path': 'YoutubeDLMaterial.Downloader.include_thumbnail'
},
'ytdl_include_metadata': {
'key': 'ytdl_include_metadata',
'path': 'YoutubeDLMaterial.Downloader.include_metadata'
},
// Extra
'ytdl_title_top': {

View File

@@ -26,11 +26,8 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
// add additional info
file_object['uid'] = uuid();
file_object['registered'] = Date.now();
path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, multiUserMode && multiUserMode.file_path);
if (!sub) {
if (multiUserMode) {
@@ -48,7 +45,13 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
}
}
const file_uid = registerFileDBManual(db_path, file_object)
const file_uid = registerFileDBManual(db_path, file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path)
}
return file_uid;
}
@@ -165,6 +168,10 @@ async function importUnregisteredFiles() {
// add subscriptions to check list
for (let i = 0; i < subscriptions_to_check.length; i++) {
let subscription_to_check = subscriptions_to_check[i];
if (!subscription_to_check.name) {
// TODO: Remove subscription as it'll never complete
continue;
}
dirs_to_check.push({
basePath: multi_user_mode ? path.join(usersFileFolder, subscription_to_check.user_uid, 'subscriptions', subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name)
: path.join(subscriptions_base_path, subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name),
@@ -175,9 +182,9 @@ async function importUnregisteredFiles() {
}
// run through check list and check each file to see if it's missing from the db
dirs_to_check.forEach(dir_to_check => {
for (const dir_to_check of dirs_to_check) {
// recursively get all files in dir's path
const files = utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
files.forEach(file => {
// check if file exists in db, if not add it
@@ -188,7 +195,7 @@ async function importUnregisteredFiles() {
logger.verbose(`Added discovered file to the database: ${file.id}`);
}
});
});
}
}

View File

@@ -1968,9 +1968,9 @@
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"node-id3": {
"version": "0.1.16",

View File

@@ -37,12 +37,13 @@
"express": "^4.17.1",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0",
"glob": "^7.1.6",
"jsonwebtoken": "^8.5.1",
"lowdb": "^1.0.0",
"md5": "^2.2.1",
"merge-files": "^0.1.2",
"multer": "^1.4.2",
"node-fetch": "^2.6.0",
"node-fetch": "^2.6.1",
"node-id3": "^0.1.14",
"nodemon": "^2.0.2",
"passport": "^0.4.1",

View File

@@ -79,17 +79,18 @@ async function getSubscriptionInfo(sub, user_uid = null) {
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
return new Promise(resolve => {
// get videos
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
// get videos
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
return new Promise(resolve => {
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (debugMode) {
logger.info('Subscribe: got info for subscription ' + sub.id);
@@ -152,39 +153,36 @@ async function getSubscriptionInfo(sub, user_uid = null) {
}
async function unsubscribe(sub, deleteMode, user_uid = null) {
return new Promise(async resolve => {
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
let result_obj = { success: false, error: '' };
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
let result_obj = { success: false, error: '' };
let id = sub.id;
if (user_uid)
users_db.get('users').find({uid: user_uid}).get('subscriptions').remove({id: id}).write();
else
db.get('subscriptions').remove({id: id}).write();
let id = sub.id;
if (user_uid)
users_db.get('users').find({uid: user_uid}).get('subscriptions').remove({id: id}).write();
else
db.get('subscriptions').remove({id: id}).write();
// failed subs have no name, on unsubscribe they shouldn't error
if (!sub.name) {
return;
}
// failed subs have no name, on unsubscribe they shouldn't error
if (!sub.name) {
return;
}
const appendedBasePath = getAppendedBasePath(sub, basePath);
if (deleteMode && fs.existsSync(appendedBasePath)) {
if (sub.archive && fs.existsSync(sub.archive)) {
const archive_file_path = path.join(sub.archive, 'archive.txt');
// deletes archive if it exists
if (fs.existsSync(archive_file_path)) {
fs.unlinkSync(archive_file_path);
}
fs.rmdirSync(sub.archive);
const appendedBasePath = getAppendedBasePath(sub, basePath);
if (deleteMode && (await fs.pathExists(appendedBasePath))) {
if (sub.archive && (await fs.pathExists(sub.archive))) {
const archive_file_path = path.join(sub.archive, 'archive.txt');
// deletes archive if it exists
if (await fs.pathExists(archive_file_path)) {
await fs.unlink(archive_file_path);
}
deleteFolderRecursive(appendedBasePath);
await fs.rmdir(sub.archive);
}
});
await fs.remove(appendedBasePath);
}
}
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
@@ -202,151 +200,154 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
const name = file;
let retrievedID = null;
sub_db.get('videos').remove({uid: file_uid}).write();
return new Promise(resolve => {
let filePath = appendedBasePath;
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
var videoFilePath = path.join(__dirname,filePath,name+ext);
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
var altImageFilePath = path.join(__dirname,filePath,name+'.jpg');
jsonExists = fs.existsSync(jsonPath);
videoFileExists = fs.existsSync(videoFilePath);
imageFileExists = fs.existsSync(imageFilePath);
altImageFileExists = fs.existsSync(altImageFilePath);
let filePath = appendedBasePath;
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
var videoFilePath = path.join(__dirname,filePath,name+ext);
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
var altImageFilePath = path.join(__dirname,filePath,name+'.jpg');
if (jsonExists) {
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
fs.unlinkSync(jsonPath);
}
const [jsonExists, videoFileExists, imageFileExists, altImageFileExists] = await Promise.all([
fs.pathExists(jsonPath),
fs.pathExists(videoFilePath),
fs.pathExists(imageFilePath),
fs.pathExists(altImageFilePath),
]);
if (imageFileExists) {
fs.unlinkSync(imageFilePath);
}
if (jsonExists) {
retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id'];
await fs.unlink(jsonPath);
}
if (altImageFileExists) {
fs.unlinkSync(altImageFilePath);
}
if (imageFileExists) {
await fs.unlink(imageFilePath);
}
if (videoFileExists) {
fs.unlink(videoFilePath, function(err) {
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
resolve(false);
} else {
// check if the user wants the video to be redownloaded (deleteForever === false)
if (!deleteForever && useArchive && sub.archive && retrievedID) {
const archive_path = path.join(sub.archive, 'archive.txt')
// if archive exists, remove line with video ID
if (fs.existsSync(archive_path)) {
removeIDFromArchive(archive_path, retrievedID);
}
}
resolve(true);
}
});
if (altImageFileExists) {
await fs.unlink(altImageFilePath);
}
if (videoFileExists) {
await fs.unlink(videoFilePath);
if ((await fs.pathExists(jsonPath)) || (await fs.pathExists(videoFilePath))) {
return false;
} else {
// TODO: tell user that the file didn't exist
resolve(true);
// check if the user wants the video to be redownloaded (deleteForever === false)
if (!deleteForever && useArchive && sub.archive && retrievedID) {
const archive_path = path.join(sub.archive, 'archive.txt')
// if archive exists, remove line with video ID
if (await fs.pathExists(archive_path)) {
await removeIDFromArchive(archive_path, retrievedID);
}
}
return true;
}
});
} else {
// TODO: tell user that the file didn't exist
return true;
}
}
async function getVideosForSub(sub, user_uid = null) {
return new Promise(resolve => {
if (!subExists(sub.id, user_uid)) {
resolve(false);
return;
if (!subExists(sub.id, user_uid)) {
return false;
}
// get sub_db
let sub_db = null;
if (user_uid)
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
else
sub_db = db.get('subscriptions').find({id: sub.id});
// get basePath
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
let appendedBasePath = null
appendedBasePath = getAppendedBasePath(sub, basePath);
let multiUserMode = null;
if (user_uid) {
multiUserMode = {
user: user_uid,
file_path: appendedBasePath
}
}
// get sub_db
let sub_db = null;
if (user_uid)
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
else
sub_db = db.get('subscriptions').find({id: sub.id});
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
// get basePath
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
let fullOutput = `${appendedBasePath}/%(title)s.%(ext)s`;
if (sub.custom_output) {
fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`;
}
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
let appendedBasePath = null
appendedBasePath = getAppendedBasePath(sub, basePath);
let qualityPath = null;
if (sub.type && sub.type === 'audio') {
qualityPath = ['-f', 'bestaudio']
qualityPath.push('-x');
qualityPath.push('--audio-format', 'mp3');
} else {
qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']
}
let multiUserMode = null;
if (user_uid) {
multiUserMode = {
user: user_uid,
file_path: appendedBasePath
}
downloadConfig.push(...qualityPath)
if (sub.custom_args) {
customArgsArray = sub.custom_args.split(',,');
if (customArgsArray.indexOf('-f') !== -1) {
// if custom args has a custom quality, replce the original quality with that of custom args
const original_output_index = downloadConfig.indexOf('-f');
downloadConfig.splice(original_output_index, 2);
}
downloadConfig.push(...customArgsArray);
}
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
let archive_dir = null;
let archive_path = null;
let fullOutput = appendedBasePath + '/%(title)s' + ext;
if (sub.custom_output) {
fullOutput = appendedBasePath + '/' + sub.custom_output + ext;
if (useArchive) {
if (sub.archive) {
archive_dir = sub.archive;
archive_path = path.join(archive_dir, 'archive.txt')
}
downloadConfig.push('--download-archive', archive_path);
}
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
// if streaming only mode, just get the list of videos
if (sub.streamingOnly) {
downloadConfig = ['-f', 'best', '--dump-json'];
}
let qualityPath = null;
if (sub.type && sub.type === 'audio') {
qualityPath = ['-f', 'bestaudio']
qualityPath.push('-x');
qualityPath.push('--audio-format', 'mp3');
if (sub.timerange) {
downloadConfig.push('--dateafter', sub.timerange);
}
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
downloadConfig.push(...qualityPath)
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
if (sub.custom_args) {
customArgsArray = sub.custom_args.split(',,');
if (customArgsArray.indexOf('-f') !== -1) {
// if custom args has a custom quality, replce the original quality with that of custom args
const original_output_index = downloadConfig.indexOf('-f');
downloadConfig.splice(original_output_index, 2);
}
downloadConfig.push(...customArgsArray);
}
// get videos
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
let archive_dir = null;
let archive_path = null;
if (useArchive) {
if (sub.archive) {
archive_dir = sub.archive;
archive_path = path.join(archive_dir, 'archive.txt')
}
downloadConfig.push('--download-archive', archive_path);
}
// if streaming only mode, just get the list of videos
if (sub.streamingOnly) {
downloadConfig = ['-f', 'best', '--dump-json'];
}
if (sub.timerange) {
downloadConfig.push('--dateafter', sub.timerange);
}
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
// get videos
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
return new Promise(resolve => {
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
logger.verbose('Subscription: finished check for ' + sub.name);
if (err && !output) {
@@ -452,23 +453,8 @@ function getAppendedBasePath(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
}
// https://stackoverflow.com/a/32197381/8088021
const deleteFolderRecursive = function(folder_to_delete) {
if (fs.existsSync(folder_to_delete)) {
fs.readdirSync(folder_to_delete).forEach((file, index) => {
const curPath = path.join(folder_to_delete, file);
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(folder_to_delete);
}
};
function removeIDFromArchive(archive_path, id) {
let data = fs.readFileSync(archive_path, {encoding: 'utf-8'});
async function removeIDFromArchive(archive_path, id) {
let data = await fs.readFile(archive_path, {encoding: 'utf-8'});
if (!data) {
logger.error('Archive could not be found.');
return;
@@ -489,7 +475,7 @@ function removeIDFromArchive(archive_path, id) {
// UPDATE FILE WITH NEW DATA
const updatedData = dataArray.join('\n');
fs.writeFileSync(archive_path, updatedData);
await fs.writeFile(archive_path, updatedData);
if (line) return line;
if (err) throw err;
}

View File

@@ -4,6 +4,7 @@ const config_api = require('./config');
const is_windows = process.platform === 'win32';
// replaces .webm with appropriate extension
function getTrueFileName(unfixed_path, type) {
let fixed_path = unfixed_path;
@@ -19,21 +20,21 @@ function getTrueFileName(unfixed_path, type) {
return fixed_path;
}
function getDownloadedFilesByType(basePath, type) {
async function getDownloadedFilesByType(basePath, type) {
// return empty array if the path doesn't exist
if (!fs.existsSync(basePath)) return [];
if (!(await fs.pathExists(basePath))) return [];
let files = [];
const ext = type === 'audio' ? 'mp3' : 'mp4';
var located_files = recFindByExt(basePath, ext);
var located_files = await recFindByExt(basePath, ext);
for (let i = 0; i < located_files.length; i++) {
let file = located_files[i];
var file_path = path.basename(file);
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
var stats = fs.statSync(file);
var stats = await fs.stat(file);
var id = file_path.substring(0, file_path.length-4);
var jsonobj = getJSONByType(type, id, basePath);
var jsonobj = await getJSONByType(type, id, basePath);
if (!jsonobj) continue;
var title = jsonobj.title;
var url = jsonobj.webpage_url;
@@ -88,13 +89,48 @@ function getJSONByType(type, name, customPath, openReadPerms = false) {
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
}
function getDownloadedThumbnail(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path') : config_api.getConfigItem('ytdl_video_folder_path');
let jpgPath = path.join(customPath, name + '.jpg');
let webpPath = path.join(customPath, name + '.webp');
let pngPath = path.join(customPath, name + '.png');
if (fs.existsSync(jpgPath))
return jpgPath;
else if (fs.existsSync(webpPath))
return webpPath;
else if (fs.existsSync(pngPath))
return pngPath;
else
return null;
}
function getExpectedFileSize(info_json) {
if (info_json['filesize']) {
return info_json['filesize'];
}
const formats = info_json['format_id'].split('+');
let expected_filesize = 0;
formats.forEach(format_id => {
info_json.formats.forEach(available_format => {
if (available_format.format_id === format_id && available_format.filesize) {
expected_filesize += available_format.filesize;
}
});
});
return expected_filesize;
}
function fixVideoMetadataPerms(name, type, customPath = null) {
if (is_windows) return;
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
: config_api.getConfigItem('ytdl_video_folder_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
const files_to_fix = [
// JSONs
path.join(customPath, name + '.info.json'),
@@ -110,27 +146,38 @@ function fixVideoMetadataPerms(name, type, customPath = null) {
}
}
function recFindByExt(base,ext,files,result)
function deleteJSONFile(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
: config_api.getConfigItem('ytdl_video_folder_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
let json_path = path.join(customPath, name + '.info.json');
let alternate_json_path = path.join(customPath, name + ext + '.info.json');
if (fs.existsSync(json_path)) fs.unlinkSync(json_path);
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
}
async function recFindByExt(base,ext,files,result)
{
files = files || fs.readdirSync(base)
files = files || (await fs.readdir(base))
result = result || []
files.forEach(
function (file) {
var newbase = path.join(base,file)
if ( fs.statSync(newbase).isDirectory() )
for (const file of files) {
var newbase = path.join(base,file)
if ( (await fs.stat(newbase)).isDirectory() )
{
result = await recFindByExt(newbase,ext,await fs.readdir(newbase),result)
}
else
{
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
{
result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result)
}
else
{
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
{
result.push(newbase)
}
result.push(newbase)
}
}
)
}
return result
}
@@ -153,7 +200,10 @@ module.exports = {
getJSONMp3: getJSONMp3,
getJSONMp4: getJSONMp4,
getTrueFileName: getTrueFileName,
getDownloadedThumbnail: getDownloadedThumbnail,
getExpectedFileSize: getExpectedFileSize,
fixVideoMetadataPerms: fixVideoMetadataPerms,
deleteJSONFile: deleteJSONFile,
getDownloadedFilesByType: getDownloadedFilesByType,
recFindByExt: recFindByExt,
File: File

View File

@@ -1,3 +0,0 @@
#!/bin/bash
# downloads a local copy of qemu on docker-hub build machines
curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .

View File

@@ -1,4 +0,0 @@
#!/bin/bash
# Register qemu-*-static for all supported processors except the
# current one, but also remove all registered binfmt_misc before
docker run --rm --privileged multiarch/qemu-user-static:register --reset

View File

@@ -38,15 +38,15 @@
</div>
<div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%">
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && router.url === '/home'" [mode]="postsService.sidepanel_mode" #sidenav>
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #sidenav>
<mat-nav-list>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</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)="sidenav.close()" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</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')))">
<mat-divider></mat-divider>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="sidenav.close()" [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>
<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>
</ng-container>
</mat-nav-list>
</mat-sidenav>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core';
import { Component, OnInit, ElementRef, ViewChild, HostBinding, AfterViewInit } from '@angular/core';
import {PostsService} from './posts.services';
import {FileCardComponent} from './file-card/file-card.component';
import { Observable } from 'rxjs/Observable';
@@ -30,11 +30,13 @@ import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dial
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
export class AppComponent implements OnInit, AfterViewInit {
@HostBinding('class') componentCssClass;
THEMES_CONFIG = THEMES_CONFIG;
window = window;
// config items
topBarTitle = 'Youtube Downloader';
defaultTheme = null;
@@ -69,6 +71,29 @@ export class AppComponent implements OnInit {
}
ngOnInit() {
if (localStorage.getItem('theme')) {
this.setTheme(localStorage.getItem('theme'));
}
this.postsService.open_create_default_admin_dialog.subscribe(open => {
if (open) {
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
dialogRef.afterClosed().subscribe(success => {
if (success) {
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
} else {
console.error('Failed to create default admin account. See logs for details.');
}
});
}
});
}
ngAfterViewInit() {
this.postsService.sidenav = this.sidenav;
}
toggleSidenav() {
this.sidenav.toggle();
}
@@ -89,9 +114,7 @@ export class AppComponent implements OnInit {
// gets the subscriptions
if (this.allowSubscriptions) {
this.postsService.getAllSubscriptions().subscribe(res => {
this.postsService.subscriptions = res['subscriptions'];
})
this.postsService.reloadSubscriptions();
}
}
@@ -124,9 +147,9 @@ export class AppComponent implements OnInit {
this.postsService.setTheme(theme);
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
}
}
onSetTheme(theme, old_theme) {
onSetTheme(theme, old_theme) {
if (old_theme) {
document.body.classList.remove(old_theme);
this.overlayContainer.getContainerElement().classList.remove(old_theme);
@@ -148,27 +171,6 @@ onSetTheme(theme, old_theme) {
event.stopPropagation();
}
ngOnInit() {
if (localStorage.getItem('theme')) {
this.setTheme(localStorage.getItem('theme'));
} else {
//
}
this.postsService.open_create_default_admin_dialog.subscribe(open => {
if (open) {
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
dialogRef.afterClosed().subscribe(success => {
if (success) {
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
} else {
console.error('Failed to create default admin account. See logs for details.');
}
});
}
});
}
getSubscriptions() {
}

View File

@@ -2,7 +2,7 @@
<div class="container">
<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' : '' ]">
<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>
<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>
</div>
</div>
</div>

View File

@@ -50,7 +50,8 @@ export class CustomPlaylistsComponent implements OnInit {
});
}
goToPlaylist(playlist) {
goToPlaylist(info_obj) {
const playlist = info_obj.file;
const playlistID = playlist.id;
const type = playlist.type;
@@ -82,7 +83,7 @@ export class CustomPlaylistsComponent implements OnInit {
const playlist = args.file;
const index = args.index;
const playlistID = playlist.id;
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
this.postsService.removePlaylist(playlistID, playlist.type).subscribe(res => {
if (res['success']) {
this.playlists.splice(index, 1);
this.postsService.openSnackBar('Playlist successfully removed.', '');

View File

@@ -31,13 +31,13 @@
<div class="container">
<div class="row justify-content-center">
<ng-container *ngIf="normal_files_received">
<div *ngFor="let file of filtered_files; 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" (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 *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>
</div>
</ng-container>
<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" [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" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
<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>
</div>
</ng-container>
</div>

View File

@@ -55,17 +55,21 @@ export class RecentVideosComponent implements OnInit {
if (localStorage.getItem('cached_file_count')) {
this.cached_file_count = +localStorage.getItem('cached_file_count');
this.loading_files = Array(this.cached_file_count).fill(0);
console.log(this.loading_files);
}
}
ngOnInit(): void {
if (this.postsService.initialized) {
this.getAllFiles();
}
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.getAllFiles();
}
});
// set filter property to cached
const cached_filter_property = localStorage.getItem('filter_property');
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
@@ -134,28 +138,36 @@ export class RecentVideosComponent implements OnInit {
// navigation
goToFile(file) {
goToFile(info_obj) {
const file = info_obj['file'];
const event = info_obj['event'];
if (this.postsService.config['Extra']['download_only_mode']) {
this.downloadFile(file);
} else {
this.navigateToFile(file);
this.navigateToFile(file, event.ctrlKey);
}
}
navigateToFile(file) {
navigateToFile(file, new_tab) {
localStorage.setItem('player_navigator', this.router.url);
if (file.sub_id) {
const sub = this.postsService.getSubscriptionByID(file.sub_id)
const sub = this.postsService.getSubscriptionByID(file.sub_id);
if (sub.streamingOnly) {
this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}]);
// streaming only mode subscriptions
!new_tab ? this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}])
: window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
} else {
this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist}]);
// normal subscriptions
!new_tab ? this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist}])
: window.open(`/#/player;fileNames=${file.id};type=${file.isAudio ? 'audio' : 'video'};subscriptionName=${sub.name};subPlaylist=${sub.isPlaylist}`);
}
} else {
this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}]);
// normal files
!new_tab ? this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}])
: window.open(`/#/player;type=${file.isAudio ? 'audio' : 'video'};uid=${file.uid}`);
}
}
@@ -177,7 +189,6 @@ export class RecentVideosComponent implements OnInit {
const type = file.isAudio ? 'audio' : 'video';
const ext = type === 'audio' ? '.mp3' : '.mp4'
const sub = this.postsService.getSubscriptionByID(file.sub_id);
console.log(sub.isPlaylist)
this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist,
this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => {
const blob: Blob = res;

View File

@@ -1,7 +1,19 @@
<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>&nbsp;&nbsp;{{file_obj.registered | date:'shortDate'}}</div>
<div (mouseover)="elevated=true" (mouseout)="elevated=false" (contextmenu)="onRightClick($event)" 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>&nbsp;&nbsp;{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</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>
<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">
<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>
@@ -25,11 +37,11 @@
<button mat-menu-item>Placeholder</button>
</ng-container>
</mat-menu>
<mat-card [matTooltip]="null" (click)="navigateToFile()" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<div style="padding:5px">
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
<div style="position: relative">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailURL" alt="Thumbnail">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailBlob ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
<div class="duration-time">
{{file_length}}
</div>

View File

@@ -103,6 +103,7 @@
background: rgba(255,255,255,0.6);
padding-left: 5px;
padding-right: 5px;
color: black;
}
.download-time {

View File

@@ -1,6 +1,22 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
import { DomSanitizer } from '@angular/platform-browser';
import { MatMenuTrigger } from '@angular/material/menu';
import { registerLocaleData } from '@angular/common';
import localeGB from '@angular/common/locales/en-GB';
import localeFR from '@angular/common/locales/fr';
import localeES from '@angular/common/locales/es';
import localeDE from '@angular/common/locales/de';
import localeZH from '@angular/common/locales/zh';
import localeNB from '@angular/common/locales/nb';
registerLocaleData(localeGB);
registerLocaleData(localeFR);
registerLocaleData(localeES);
registerLocaleData(localeDE);
registerLocaleData(localeZH);
registerLocaleData(localeNB);
@Component({
selector: 'app-unified-file-card',
@@ -16,6 +32,10 @@ export class UnifiedFileCardComponent implements OnInit {
type = null;
elevated = false;
// optional vars
thumbnailBlobURL = null;
// input/output
@Input() loading = true;
@Input() theme = null;
@Input() file_obj = null;
@@ -23,10 +43,15 @@ export class UnifiedFileCardComponent implements OnInit {
@Input() use_youtubedl_archive = false;
@Input() is_playlist = false;
@Input() index: number;
@Input() locale = null;
@Output() goToFile = new EventEmitter<any>();
@Output() goToSubscription = new EventEmitter<any>();
@Output() deleteFile = new EventEmitter<any>();
@Output() editPlaylist = new EventEmitter<any>();
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
contextMenuPosition = { x: '0px', y: '0px' };
/*
Planned sizes:
@@ -35,12 +60,19 @@ export class UnifiedFileCardComponent implements OnInit {
big: 250x200
*/
constructor(private dialog: MatDialog) { }
constructor(private dialog: MatDialog, private sanitizer: DomSanitizer) { }
ngOnInit(): void {
if (!this.loading) {
this.file_length = fancyTimeFormat(this.file_obj.duration);
}
if (this.file_obj && this.file_obj.thumbnailBlob) {
const mime = getMimeByFilename(this.file_obj.thumbnailPath);
const blob = new Blob([new Uint8Array(this.file_obj.thumbnailBlob.data)], {type: mime});
const bloburl = URL.createObjectURL(blob);
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);
}
}
emitDeleteFile(blacklistMode = false) {
@@ -51,8 +83,8 @@ export class UnifiedFileCardComponent implements OnInit {
});
}
navigateToFile() {
this.goToFile.emit(this.file_obj);
navigateToFile(event) {
this.goToFile.emit({file: this.file_obj, event: event});
}
navigateToSubscription() {
@@ -75,6 +107,15 @@ export class UnifiedFileCardComponent implements OnInit {
});
}
onRightClick(event) {
event.preventDefault();
this.contextMenuPosition.x = event.clientX + 'px';
this.contextMenuPosition.y = event.clientY + 'px';
this.contextMenu.menuData = { 'item': {id: 1, name: 'hi'} };
this.contextMenu.menu.focusFirstItem('mouse');
this.contextMenu.openMenu();
}
}
function fancyTimeFormat(time) {
@@ -97,3 +138,16 @@ function fancyTimeFormat(time) {
ret += '' + secs;
return ret;
}
function getMimeByFilename(name) {
switch (name.substring(name.length-4, name.length)) {
case '.jpg':
return 'image/jpeg';
case '.png':
return 'image/png';
case 'webp':
return 'image/webp';
default:
return null;
}
}

View File

@@ -17,7 +17,7 @@
</ng-template>
</ngx-file-drop>
<div style="margin-top: 15px;">
<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>
<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>
</div>
<div style="margin-top: 10px;">
<table class="table">

View File

@@ -1,4 +1,4 @@
<h4 mat-dialog-title i18n="Edit subscription dialog title">Editing {{sub.name}}</h4>
<h4 mat-dialog-title i18n="Edit subscription dialog title prefix">Editing</h4>&nbsp;{{sub.name}}
<mat-dialog-content>
<div class="container-fluid">

View File

@@ -14,7 +14,7 @@
</mat-button-toggle-group>
<div class="add-content-button">
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu">Add more 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>
</div>
<mat-menu #menu="matMenu">
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file}}</button>
@@ -24,5 +24,5 @@
<mat-dialog-actions>
<!-- Save -->
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent">Save</button>
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent"><ng-container i18n="Save">Save</ng-container></button>
</mat-dialog-actions>

View File

@@ -164,7 +164,7 @@
<br/>
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile">
<div class="margined">
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 15;else indeterminateprogress">
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
<br/>
</div>
@@ -181,10 +181,12 @@
</ng-template>
<app-recent-videos></app-recent-videos>
<br/>
<h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
<app-recent-videos></app-recent-videos>
<br/>
<h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>
</ng-container>
<!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
<mat-accordion>

View File

@@ -82,8 +82,9 @@ export class MainComponent implements OnInit {
useDefaultDownloadingAgent = true;
customDownloadingAgent = null;
// formats cache
// cache
cachedAvailableFormats = {};
cachedFileManagerEnabled = localStorage.getItem('cached_filemanager_enabled') === 'true';
// youtube api
youtubeSearchEnabled = false;
@@ -232,7 +233,8 @@ export class MainComponent implements OnInit {
async loadConfig() {
// loading config
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled'];
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled']
&& (!this.postsService.isLoggedIn || this.postsService.permissions.includes('filemanager'));
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
this.allowMultiDownloadMode = this.postsService.config['Extra']['allow_multi_download_mode'];
this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
@@ -261,6 +263,10 @@ export class MainComponent implements OnInit {
}
// set final cache items
localStorage.setItem('cached_filemanager_enabled', this.fileManagerEnabled.toString());
this.cachedFileManagerEnabled = this.fileManagerEnabled;
if (this.allowAdvancedDownload) {
if (localStorage.getItem('customArgsEnabled') !== null) {
this.customArgsEnabled = localStorage.getItem('customArgsEnabled') === 'true';
@@ -1033,8 +1039,8 @@ export class MainComponent implements OnInit {
}
} else if (format_obj.type === 'video') {
// check if video format is mp4
const key = format.height.toString();
if (format.ext === 'mp4') {
const key = format.format_note.replace('p', '');
if (format.ext === 'mp4' || format.ext === 'mkv' || format.ext === 'webm') {
format_obj['height'] = format.height;
format_obj['acodec'] = format.acodec;
format_obj['format_id'] = format.format_id;

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, HostListener, EventEmitter, OnDestroy } from '@angular/core';
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit } from '@angular/core';
import { VgAPI } from 'ngx-videogular';
import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router';
@@ -20,7 +20,7 @@ export interface IMedia {
templateUrl: './player.component.html',
styleUrls: ['./player.component.css']
})
export class PlayerComponent implements OnInit, OnDestroy {
export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
playlist: Array<IMedia> = [];
original_playlist: string = null;
@@ -95,6 +95,10 @@ export class PlayerComponent implements OnInit, OnDestroy {
}
}
ngAfterViewInit() {
this.postsService.sidenav.close();
}
ngOnDestroy() {
// prevents volume save feature from running in the background
clearInterval(this.save_volume_timer);
@@ -211,7 +215,7 @@ export class PlayerComponent implements OnInit, OnDestroy {
// adds user token if in multi-user-mode
const uuid_str = this.uuid ? `&uuid=${this.uuid}` : '';
const uid_str = (this.id || !this.db_file) ? '' : `&uid=${this.db_file.uid}`;
const type_str = (this.id || !this.db_file) ? '' : `&type=${this.db_file.type}`
const type_str = (this.id || !this.db_file || !this.db_file.type) ? '' : `&type=${this.db_file.type}`
const id_str = this.id ? `&id=${this.id}` : '';
if (this.postsService.isLoggedIn) {
fullLocation += (this.subscriptionName ? '&' : '?') + `jwt=${this.postsService.token}`;
@@ -313,7 +317,8 @@ export class PlayerComponent implements OnInit, OnDestroy {
const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
this.downloading = true;
this.postsService.downloadFileFromServer(fileNames, this.type, zipName).subscribe(res => {
this.postsService.downloadFileFromServer(fileNames, this.type, zipName, null, null, null, null,
!this.uuid ? this.postsService.user.uid : this.uuid, this.id).subscribe(res => {
this.downloading = false;
const blob: Blob = res;
saveAs(blob, zipName + '.zip');

View File

@@ -11,20 +11,19 @@ import { BehaviorSubject } from 'rxjs';
import { v4 as uuid } from 'uuid';
import { MatSnackBar } from '@angular/material/snack-bar';
import * as Fingerprint2 from 'fingerprintjs2';
import { isoLangs } from './settings/locales_list';
@Injectable()
export class PostsService implements CanActivate {
path = '';
audioFolder = '';
videoFolder = '';
startPath = null; // 'http://localhost:17442/';
startPathSSL = null; // 'https://localhost:17442/'
handShakeComplete = false;
// local settings
THEMES_CONFIG = THEMES_CONFIG;
theme;
card_size = 'medium';
sidepanel_mode = 'over';
settings_changed = new BehaviorSubject<boolean>(false);
// auth
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
session_id = null;
httpOptions = null;
@@ -41,20 +40,25 @@ export class PostsService implements CanActivate {
available_permissions = null;
// behavior subjects
reload_config = new BehaviorSubject<boolean>(false);
config_reloaded = new BehaviorSubject<boolean>(false);
service_initialized = new BehaviorSubject<boolean>(false);
initialized = false;
settings_changed = new BehaviorSubject<boolean>(false);
open_create_default_admin_dialog = new BehaviorSubject<boolean>(false);
// app status
initialized = false;
// global vars
config = null;
subscriptions = null;
sidenav = null;
locale = isoLangs['en'];
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
public snackBar: MatSnackBar) {
console.log('PostsService Initialized...');
// this.startPath = window.location.href + '/api/';
// this.startPathSSL = window.location.href + '/api/';
this.path = this.document.location.origin + '/api/';
if (isDevMode()) {
@@ -112,6 +116,17 @@ export class PostsService implements CanActivate {
if (localStorage.getItem('card_size')) {
this.card_size = localStorage.getItem('card_size');
}
// localization
const locale = localStorage.getItem('locale');
if (!locale) {
localStorage.setItem('locale', 'en');
}
if (isoLangs[locale]) {
this.locale = isoLangs[locale];
}
}
canActivate(route, state): Promise<boolean> {
return new Promise(resolve => {
@@ -152,14 +167,6 @@ export class PostsService implements CanActivate {
});
}
getVideoFolder() {
return this.http.get(this.startPath + 'videofolder');
}
getAudioFolder() {
return this.http.get(this.startPath + 'audiofolder');
}
// tslint:disable-next-line: max-line-length
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) {
return this.http.post(this.path + 'tomp3', {url: url,
@@ -229,7 +236,7 @@ export class PostsService implements CanActivate {
}
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
uid = null, uuid = null) {
uid = null, uuid = null, id = null) {
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
type: type,
zip_mode: Array.isArray(fileName),
@@ -238,7 +245,8 @@ export class PostsService implements CanActivate {
subscriptionName: subscriptionName,
subPlaylist: subPlaylist,
uuid: uuid,
uid: uid
uid: uid,
id: id
},
{responseType: 'blob', params: this.httpOptions.params});
}
@@ -398,6 +406,8 @@ export class PostsService implements CanActivate {
}, err => {
if (err.status === 401) {
this.sendToLogin();
this.token = null;
this.resetHttpParams();
}
console.log(err);
});
@@ -414,14 +424,7 @@ export class PostsService implements CanActivate {
this.router.navigate(['/login']);
}
// resets http params
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
this.httpOptions = {
params: new HttpParams({
fromString: this.http_params
}),
};
this.resetHttpParams();
}
// user methods
@@ -446,12 +449,29 @@ export class PostsService implements CanActivate {
this.openSnackBar('You must log in to access this page!');
}
resetHttpParams() {
// resets http params
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
this.httpOptions = {
params: new HttpParams({
fromString: this.http_params
}),
};
}
setInitialized() {
this.service_initialized.next(true);
this.initialized = true;
this.config_reloaded.next(true);
}
reloadSubscriptions() {
this.getAllSubscriptions().subscribe(res => {
this.subscriptions = res['subscriptions'];
});
}
adminExists() {
return this.http.post(this.path + 'auth/adminExists', {}, this.httpOptions);
}

View File

@@ -122,7 +122,8 @@ export const isoLangs = {
},
'zh': {
'name': 'Chinese',
'nativeName': '中文 (Zhōngwén), 汉语, 漢語'
'nativeName': '中文 (Zhōngwén), 汉语, 漢語',
'ngID': 'zh'
},
'cv': {
'name': 'Chuvash',
@@ -162,8 +163,14 @@ export const isoLangs = {
},
'en': {
'name': 'English',
'nativeName': 'English'
'nativeName': 'English',
'ngID': 'en-US'
},
'en-GB': {
'name': 'British English',
'nativeName': 'British English',
'ngID': 'en-GB'
},
'eo': {
'name': 'Esperanto',
'nativeName': 'Esperanto'
@@ -190,7 +197,8 @@ export const isoLangs = {
},
'fr': {
'name': 'French',
'nativeName': 'français, langue française'
'nativeName': 'français',
'ngID': 'fr'
},
'ff': {
'name': 'Fula; Fulah; Pulaar; Pular',
@@ -206,7 +214,8 @@ export const isoLangs = {
},
'de': {
'name': 'German',
'nativeName': 'Deutsch'
'nativeName': 'Deutsch',
'ngID': 'de'
},
'el': {
'name': 'Greek, Modern',
@@ -438,7 +447,8 @@ export const isoLangs = {
},
'nb': {
'name': 'Norwegian Bokmål',
'nativeName': 'Norsk bokmål'
'nativeName': 'Norsk bokmål',
'ngID': 'nb'
},
'nd': {
'name': 'North Ndebele',
@@ -594,7 +604,8 @@ export const isoLangs = {
},
'es': {
'name': 'Spanish; Castilian',
'nativeName': 'español'
'nativeName': 'español',
'ngID': 'es'
},
'su': {
'name': 'Sundanese',

View File

@@ -128,7 +128,11 @@
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['safe_download_override']"><ng-container i18n="Safe download override setting">Safe download override</ng-container></mat-checkbox>
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_thumbnail']"><ng-container i18n="Include thumbnail setting">Include thumbnail</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_metadata']"><ng-container i18n="Include metadata setting">Include metadata</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2">
@@ -249,7 +253,7 @@
</div>
<div class="col-12 mt-2 mb-1">
<mat-form-field>
<mat-label><ng-container i18n="Logger level select label">Select a logger level</ng-container></mat-label>
<mat-label><ng-container i18n="Log Level label">Log Level</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['logger_level']">
<mat-option value="debug">Debug</mat-option>
<mat-option value="verbose">Verbose</mat-option>

View File

@@ -17,7 +17,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo
})
export class SettingsComponent implements OnInit {
all_locales = isoLangs;
supported_locales = ['en', 'es', 'de'];
supported_locales = ['en', 'es', 'de', 'fr', 'zh', 'nb', 'en-GB'];
initialLocale = localStorage.getItem('locale');
initial_config = null;

View File

@@ -108,7 +108,7 @@ export class SubscriptionComponent implements OnInit {
} else {
this.router.navigate(['/player', {fileNames: name,
type: this.subscription.type ? this.subscription.type : 'video', subscriptionName: this.subscription.name,
subPlaylist: this.subscription.isPlaylist, uuid: this.postsService.user ? this.postsService.user.uid : null}]);
subPlaylist: this.subscription.isPlaylist}]);
}
}

View File

@@ -80,6 +80,7 @@ export class SubscriptionsComponent implements OnInit {
} else {
this.channel_subscriptions.push(result);
}
this.postsService.reloadSubscriptions();
}
});
}
@@ -96,6 +97,7 @@ export class SubscriptionsComponent implements OnInit {
if (success) {
this.openSnackBar(`${sub.name} successfully deleted!`)
this.getSubscriptions();
this.postsService.reloadSubscriptions();
}
})
}

View File

@@ -1,5 +1,5 @@
{
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Playlist erstellen",
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Wiedergabeliste erstellen",
"cff1428d10d59d14e45edec3c735a27b5482db59": "Name",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiodateien",
"a52dae09be10ca3a65da918533ced3d3f4992238": "Videos",
@@ -35,7 +35,7 @@
"c32ef07f8803a223a83ed17024b38e8d82292407": "Passwort",
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
"9779715ac05308973d8f1c8658b29b986e92450f": "Ihre Audiodateien befinden sich hier",
"47546e45bbb476baaaad38244db444c427ddc502": "Playlisten",
"47546e45bbb476baaaad38244db444c427ddc502": "Wiedergabelisten",
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Keine Wiedergabelisten verfügbar. Erstellen Sie eine aus Ihren heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Video",
"960582a8b9d7942716866ecfb7718309728f2916": "Ihre Videodateien befinden sich hier",
@@ -83,7 +83,7 @@
"46826331da1949bd6fb74624447057099c9d20cd": "Video Basispfad",
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Dateipfad für Video-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.",
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Globale benutzerdefinierte Argumente für Downloads auf der Startseite. Argumente werden durch zwei Kommata voneinander getrennt: ,,",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Herunterlader",
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Titel der Kopfzeile",
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Dateimanager aktivieren",
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Download-Manager aktivieren",
@@ -117,14 +117,14 @@
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Speichern",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Schließen} false {Abbrechen} other {Andere}}",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Über YoutubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "ist ein Open-Source YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "ist ein quelloffener YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "beinhaltet viele tolle Funktionen! API, Docker und Lokalisierung werden unter anderem unterstützt. Informieren Sie sich über alle unterstützten Funktionen auf Github.",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Installierte Version:",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Suche nach Updates ...",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Aktualisierung verfügbar",
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Sie können über das Einstellungsmenü aktualisieren.",
"b33536f59b94ec935a16bd6869d836895dc5300c": "Haben Sie einen Fehler gefunden oder einen Vorschlag?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "um ein Ticket zu öffnen.",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "um ein Ticket zu öffnen!",
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Ihr Profil",
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Erstellt:",
@@ -138,8 +138,8 @@
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Über",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Startseite",
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements",
"822fab38216f64e8166d368b59fe756ca39d301b": "Downloads",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Playlist teilen",
"822fab38216f64e8166d368b59fe756ca39d301b": "Heruntergeladene",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Wiedergabeliste teilen",
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Video teilen",
"1d540dcd271b316545d070f9d182c372d923aadd": "Audio teilen",
"1f6d14a780a37a97899dc611881e6bc971268285": "Freigabe aktivieren",
@@ -152,8 +152,8 @@
"77b0c73840665945b25bd128709aa64c8f017e1c": "Download Start:",
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Download Ende:",
"ad127117f9471612f47d01eae09709da444a36a4": "Dateipfad(e):",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Playlist oder einen Kanal",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "Playlist oder Kanal URL",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Wiedergabeliste oder einen Kanal",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "URL der Wiedergabeliste oder des Kanales",
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Benutzerdefinierter Name",
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Dies ist optional",
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle Uploads herunterladen",
@@ -168,18 +168,18 @@
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanäle",
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Name nicht verfügbar. Kanal wird abgerufen...",
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Sie haben keine Kanäle abonniert.",
"2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Playlist wird abgerufen...",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Playlisten abonniert.",
"2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Wiedergabeliste wird abgerufen.",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Wiedergabeliste abonniert.",
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Suchen",
"2054791b822475aeaea95c0119113de3200f5e1c": "Länge:",
"94e01842dcee90531caa52e4147f70679bac87fe": "Löschen und erneut herunterladen",
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent löschen",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Aktualisierungsprogramm",
"1372e61c5bd06100844bd43b98b016aabc468f62": "Wählen Sie eine Version:",
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrieren",
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sitzungs-ID:",
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(aktuell)",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Downloads verfügbar.",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Herunterladen-Ereignisse verfügbar!",
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
@@ -194,5 +194,32 @@
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Aktionen",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Benutzer hinzufügen",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rolle bearbeiten"
}
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rolle bearbeiten",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Zeilen:",
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Logs erscheinen hier",
"98b6ec9ec138186d663e64770267b67334353d63": "Benutzerdefinierte Dateiausgabe",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Diese werden nach den Standardargumenten hinzugefügt.",
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Nur-Audio Modus",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Protokolle",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Cookies setzen",
"431e5f3a0dde88768d1074baedd65266412b3f02": "Cookies verwenden",
"d01715b75228878a773ae6d059acc639d4898a03": "Safe download aufheben",
"85e0725c870b28458fd3bbba905397d890f00a69": "Beachte: Neu hochgeladene Cookies überschreiben die vorherigen. Cookies sind global und gelten nicht auf einer Pro-Benutzer-Basis.",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Ziehen-und-Ablegen",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Neue Cookies hochladen",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Bearbeiten",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Wiedergabeliste bearbeiten",
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Suchfilter",
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Intern",
"db6c192032f4cab809aad35215f0aa4765761897": "Ablauf der Anmeldung",
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Protokollebene",
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Dadurch wird Ihr alter API-Schlüssel gelöscht!",
"fb35145bfb84521e21b6385363d59221f436a573": "Alle Herunterladen-Ereignisse abbrechen",
"384de8f8f112c9e6092eb2698706d391553f3e8d": "Metadaten einschließen",
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Miniaturansicht einschließen",
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "HINWEIS: Durch das Hochladen neuer Cookies werden Ihre vorherigen Cookies überschrieben. Beachten Sie auch, dass Cookies instanzweit und nicht pro Benutzer sind.",
"511b600ae4cf037e4eb3b7a58410842cd5727490": "Weitere Inhalte hinzufügen",
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Typ",
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Video",
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio"
}

File diff suppressed because it is too large Load Diff

View File

@@ -197,19 +197,49 @@
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modify playlist",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editar",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Sube nuevas cookies",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrastrar y soltar",
"85e0725c870b28458fd3bbba905397d890f00a69": "NOTA: Cargar nuevas cookies anulará sus cookies anteriores. También tenga en cuenta que las cookies son de toda la instancia, no por usuario.",
"d01715b75228878a773ae6d059acc639d4898a03": "Anulación de descarga segura",
"00e274c496b094a019f0679c3fab3945793f3335": "Seleccione un nivel de registrador",
"431e5f3a0dde88768d1074baedd65266412b3f02": "Utilizar Cookies",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Establecer Cookies",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registros",
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Solo audio",
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Search Base",
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Search Filter",
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Bind Credentials",
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Bind DN",
"1db9789b93069861019bd0ccaa5d4706b00afc61": "URL LDAP",
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "Método de autenticación",
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Interno",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Configurar Cookies",
"431e5f3a0dde88768d1074baedd65266412b3f02": "Usar Cookies",
"db6c192032f4cab809aad35215f0aa4765761897": "Caducidad de inicio de sesión",
"00e274c496b094a019f0679c3fab3945793f3335": "Seleccione un nivel de registrador",
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "¡Esto eliminará su vieja clave API!",
"fb35145bfb84521e21b6385363d59221f436a573": "Mata todas las descargas",
"384de8f8f112c9e6092eb2698706d391553f3e8d": "Incluir metadatos",
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Incluir miniatura",
"85e0725c870b28458fd3bbba905397d890f00a69": "NOTA: La carga de cookies nuevas anulará las cookies anteriores y las cookies son para toda la instancia, no por usuario.",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrastrar y soltar",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Subir nuevas cookies",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editar",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modificar lista de reproducción",
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Tipo",
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Vídeo",
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio",
"d02888c485d3aeab6de628508f4a00312a722894": "Mis videos",
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Ir a suscripción",
"5656a06f17c24b2d7eae9c221567b209743829a9": "Abrir archivo en nueva pestaña",
"ccf5ea825526ac490974336cb5c24352886abc07": "Abrir archivo",
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Borrar registros",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Líneas:",
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Eliminar usuario",
"632e8b20c98e8eec4059a605a4b011bb476137af": "Editar usuario",
"98b6ec9ec138186d663e64770267b67334353d63": "Salida de archivo personalizado",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Estos se agregan después de los argumentos estándar.",
"98b6ec9ec138186d663e64770267b67334353d63": "Salida de archivo personalizada",
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Los registros aparecerán aquí",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Líneas:"
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Modo de solo audio",
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Un error ha ocurrido:",
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "Un error ha ocurrido",
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "La descarga era exitosa",
"544e09cdc99a8978f48521d45f62db0da6dcf742": "Usar rol predeterminado",
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Claro todas las descargas",
"3697f8583ea42868aa269489ad366103d94aece7": "Editando",
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Nivel de registro",
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "NOTA: La carga de cookies nuevas anulará las cookies anteriores y las cookies son para toda la instancia, no por usuario.",
"511b600ae4cf037e4eb3b7a58410842cd5727490": "Agregar más contenido"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,228 @@
{
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Créer une liste de lecture",
"cff1428d10d59d14e45edec3c735a27b5482db59": "Nom",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Fichiers audio",
"a52dae09be10ca3a65da918533ced3d3f4992238": "Vidéos",
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modifier les args de téléchargement",
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Nouveaux args simulés",
"0b71824ae71972f236039bed43f8d2323e8fd570": "Ajouter un arg",
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Rechercher par catégorie",
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Utiliser la valeur arg",
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Valeur arg",
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Ajouter",
"d7b35c384aecd25a516200d6921836374613dfe7": "Annuler",
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modifier",
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "Youtube Downloader",
"a38ae1082fec79ba1f379978337385a539a28e73": "Résolution",
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Utiliser l'URL",
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Voir",
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Audio seulement",
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Activer le téléchargement simultané",
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Télécharger",
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Annuler",
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Système",
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Prévisualisation des arguments :",
"4e4c721129466be9c3862294dc40241b64045998": "Utiliser des arguments personnalisés",
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Arguments personnalisés",
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Pas besoin d'inclure l'URL, seulement ce qui suit. Les arguments sont délimités par deux virgules comme suit ,,",
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Modifier le chemin de sortie",
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Sortie personnalisée",
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentation",
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "Le chemin est relatif au chemin de téléchargement de la config. Ne pas inclure l'extension.",
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "S'authentifier",
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Identifiant",
"c32ef07f8803a223a83ed17024b38e8d82292407": "Mot de Passe",
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
"9779715ac05308973d8f1c8658b29b986e92450f": "Vos fichiers audio sont ici",
"47546e45bbb476baaaad38244db444c427ddc502": "Listes de lecture",
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Aucune liste de lecture disponible. Créez-en une à l'aide du bouton \\\"+\\\" bleu de votre fichier audio.",
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Vídéos",
"960582a8b9d7942716866ecfb7718309728f2916": "Vos fichiers vidéos sont ici",
"0f59c46ca29e9725898093c9ea6b586730d0624e": "Aucune liste de lecture disponible. Créez-en une à l'aide du bouton \\\"+\\\" bleu de votre fichier vidéo.",
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Nom :",
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL :",
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Uploader :",
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Taille du fichier :",
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Chemin :",
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Mise en ligne :",
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Fermer",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modifier la liste de lecture",
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID :",
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Compteur :",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editer",
"826b25211922a1b46436589233cb6f1a163d89b7": "Effacer",
"321e4419a943044e674beb55b8039f42a9761ca5": "Informations",
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Supprimer et bannir",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Téléverser de nouveaux cookies",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Glisser-déposer",
"85e0725c870b28458fd3bbba905397d890f00a69": "NOTE : le téléversement de nouveaux cookies remplacera vos cookies précédents. Notez également que les cookies sont par instance, et non par utilisateur.",
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Paramètres",
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL à partir de laquelle cette application sera accessible, sans le port.",
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Port",
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Port souhaité. La valeur par défault est 17442.",
"d4477669a560750d2064051a510ef4d7679e2f3e": "Activer Multi-Utilisateurs",
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Chemin d'enregistrement des utilisateurs",
"a64505c41150663968e277ec9b3ddaa5f4838798": "Chemin racine pour les utilisateurs et leurs vidéos téléchargées.",
"cbe16a57be414e84b6a68309d08fad894df797d6": "Activer le cryptage des données",
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Chemin du fichier de certificat",
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "Chemin d'accès au fichier clé",
"4e3120311801c4acd18de7146add2ee4a4417773": "Autoriser les abonnements",
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Chemin d'enregistrement des abonnements",
"bc9892814ee2d119ae94378c905ea440a249b84a": "Chemin racine pour les vidéos des chaînes et des listes de lecture auxquelles vous êtes abonné.",
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Interval de vérification des abonnements",
"0f56a7449b77630c114615395bbda4cab398efd8": "L'unité est la seconde, écricre uniquement un nombre entier.",
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Utiliser youtube-dl archive",
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Avec youtube-dl's archive",
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "les vidéos téléchargées à partir de vos abonnements sont enregistrées dans un fichier texte dans le sous-répertoire du fichier d'abonnement.",
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "Cela vous permet de supprimer définitivement des vidéos de vos abonnements sans vous désabonner et vous permet d'enregistrer les vidéos que vous avez téléchargées en cas de perte de données.",
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Thème",
"ff7cee38a2259526c519f878e71b964f41db4348": "Default",
"adb4562d2dbd3584370e44496969d58c511ecb63": "Sombre",
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Autoriser le changement du thème",
"fe46ccaae902ce974e2441abe752399288298619": "Choix de la langue",
"82421c3e46a0453a70c42900eab51d58d79e6599": "Principal",
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Chemin du dossier audio",
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Chemin racine pour les téléchargements audio uniquement.",
"46826331da1949bd6fb74624447057099c9d20cd": "Chemin d'accès au dossier vidéo",
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Chemin racine de téléchargement des vidéos.",
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Arguments personnalisés globaux pour les téléchargements sur la page d'accueil. Les arguments sont délimités par deux virgules comme suit ,,",
"d01715b75228878a773ae6d059acc639d4898a03": "Désactiver le téléchargement sécurisé",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Téléchargements",
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Nom de l'application",
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Gestionnaire de fichiers activé",
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Gestionnaire de téléchargement activé",
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Permettre la sélection de la résolution",
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Empêcher le téléchargement local",
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Autoriser les téléchargements simultanés",
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Utiliser un code PIN",
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Définir le code PIN",
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Activer l'API publique",
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Clé API publique",
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Voir documentation",
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Générer",
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Utiliser l'API YouTube",
"ce10d31febb3d9d60c160750570310f303a22c22": "Clé API YouTube",
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Générer un mot de passe !",
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Cliquez ici",
"7f09776373995003161235c0c8d02b7f91dbc4df": "pour télécharger manuellement l'extension officielle YoutubeDL-Material Chrome.",
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Vous devez charger manuellement l'extension et modifier les paramètres de l'extension pour définir l'URL de l'interface.",
"9a2ec6da48771128384887525bdcac992632c863": "pour installer l'extension officielle YoutubeDL-Material Firefox directement depuis la page des extensions Firefox.",
"eb81be6b49e195e5307811d1d08a19259d411f37": "Installation détaillé ici.",
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Il suffit de modifier les paramètres de l'extension pour définir l'URL de l'interface.",
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Faites glisser le lien ci-dessous vers vos favoris, et le tour est joué ! Il suffit de naviguer vers la vidéo YouTube que vous souhaitez télécharger et de cliquer sur le signet.",
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Générer un signet audio uniquement",
"d5f69691f9f05711633128b5a3db696783266b58": "Avancés",
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Utiliser l'agent de téléchargement par défault",
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Sélectionner une méthode de téléchargement",
"00e274c496b094a019f0679c3fab3945793f3335": "Niveau des logs",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Autoriser le téléchargement avancé",
"431e5f3a0dde88768d1074baedd65266412b3f02": "Utiliser les Cookies",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Gérer les Cookies",
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avancé",
"37224420db54d4bc7696f157b779a7225f03ca9d": "Autoriser l'enregistrement des utilisateurs",
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Utilisateurs",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Journaux",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Sauvegarder",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Fermer} false {Annuler}}",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Sobre YoutubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "est un téléchargeur YouTube open source créé selon le graphique \\\"Material Design\\\" de Google. Vous pouvez facilement télécharger vos vidéos préférées sous forme de fichiers vidéo ou audio, et même vous abonner à vos chaînes et listes de lecture préférées pour vous tenir au courant de vos nouvelles vidéos.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "comporte des caractéristiques étonnantes ! Une API étendue, un support Docker et un support de localisation (traduction). Pour en savoir plus sur toutes les fonctionnalités prises en charge, cliquez sur l'icône GitHub ci-dessus.",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Version installée :",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Vérification des mises à jours ...",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Mise à jour disponible",
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Vous pouvez mettre à jour à partir du menu de configuration.",
"b33536f59b94ec935a16bd6869d836895dc5300c": "Avez-vous trouvé une erreur ou avez-vous une suggestion ?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "pour signaler un problème !",
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Votre profil",
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID :",
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Créé le :",
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Vous n'êtes pas identifié.",
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Identifiant",
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Déconnexion",
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Créer un compte administrateur",
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Aucun compte administrateur détecté. Veuillez définir le mot de passe du compte adminstrateur \\\"admin\\\".",
"70a67e04629f6d412db0a12d51820b480788d795": "Créer",
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profil",
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "À propos",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Accueil",
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements",
"822fab38216f64e8166d368b59fe756ca39d301b": "Téléchargements",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Partager une liste de lecture",
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Partager vidéo",
"1d540dcd271b316545d070f9d182c372d923aadd": "Partager audio",
"1f6d14a780a37a97899dc611881e6bc971268285": "Activer le partage",
"6580b6a950d952df847cb3d8e7176720a740adc8": "Utiliser l'horodatage",
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Secondes",
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Copier dans le presse-papiers",
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Sauvegarder",
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Détails",
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Une erreur s'est produite :",
"77b0c73840665945b25bd128709aa64c8f017e1c": "Début du téléchargement :",
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Fin du téléchargement :",
"ad127117f9471612f47d01eae09709da444a36a4": "Chemin(s) de fichier :",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "S'abonner à la liste de lecture ou à la chaîne",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "L'URL de la liste de lecture ou de la chaîne",
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nom personnalisé",
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Télécharger tous les fichiers téléversés",
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Télécharger les dernières vidéos téléversées",
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Extraire le son",
"408ca4911457e84a348cecf214f02c69289aa8f1": "Streaming uniquement",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Ceux-ci sont ajoutés après les arguments standard.",
"98b6ec9ec138186d663e64770267b67334353d63": "Sortie de fichier personnalisé",
"d0336848b0c375a1c25ba369b3481ee383217a4f": "S'abonner",
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Type :",
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archive :",
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Exporter l'archive",
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Se désabonner",
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Vos abonnements",
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Chaînes",
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Nom non disponible. Le rétablissement des canaux est en cours...",
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Vous n'êtes abonné à aucune châine.",
"2e0a410652cb07d069f576b61eab32586a18320d": "Ce nom n'est pas disponible. Recherche de chaînes en cours.",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Vous n'êtes abonné·e à aucune liste de lectures.",
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Chercher",
"2054791b822475aeaea95c0119113de3200f5e1c": "Durée :",
"94e01842dcee90531caa52e4147f70679bac87fe": "Effacer et re-télécharger",
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Supprimer définitivement",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Mettre à jour",
"1372e61c5bd06100844bd43b98b016aabc468f62": "Sélectionnez une version :",
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Créer un compte",
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID de la session :",
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(actual)",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Pas de téléchargements !",
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Ajouter un utilisateur",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Identifiant",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Gérer l'utilisateur",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Identifiant de l'utilisateur :",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nouveau mot de passe",
"6498fa1b8f563988f769654a75411bb8060134b9": "Enregistrer le nouveau mot de passe",
"40da072004086c9ec00d125165da91eaade7f541": "Utilisation par défault",
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Oui",
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Non",
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Gérer le groupe",
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Identifiant",
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Groupe",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Gérer",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Ajouter",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Gérer les groupes",
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Les enregistrements apparaîtront ici",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Lignes :",
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "Une erreur s'est produite",
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Le téléchargement a réussi",
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "Méthode d'authentification",
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Filtre de recherche",
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Interne",
"db6c192032f4cab809aad35215f0aa4765761897": "Expiration de la connexion",
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Niveau de journal",
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Ceci supprimera votre ancienne clé API !",
"fb35145bfb84521e21b6385363d59221f436a573": "Supprimer tous les téléchargements",
"384de8f8f112c9e6092eb2698706d391553f3e8d": "Inclure les métadonnées",
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Inclure une miniature",
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "REMARQUE : le téléversement de nouveaux cookies remplacera vos cookies précédents. Notez également que les cookies sont à l'échelle de l'instance et non par utilisateur.",
"511b600ae4cf037e4eb3b7a58410842cd5727490": "Ajouter plus de contenu",
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Type",
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Vidéo",
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio",
"3697f8583ea42868aa269489ad366103d94aece7": "Édition"
}

View File

@@ -0,0 +1,222 @@
{
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Opprett en spilleliste",
"cff1428d10d59d14e45edec3c735a27b5482db59": "Navn",
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Lyd",
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Video",
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Type",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Lydfiler",
"a52dae09be10ca3a65da918533ced3d3f4992238": "Videoer",
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Endre youtube-dl-argumenter",
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Simulerte nye argumenter",
"0b71824ae71972f236039bed43f8d2323e8fd570": "Legg til argument",
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Søk etter kategori",
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Bruk argument-verdi",
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Argument-verdi",
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Legg til argument",
"d7b35c384aecd25a516200d6921836374613dfe7": "Avbryt",
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Endre",
"a38ae1082fec79ba1f379978337385a539a28e73": "Kvalitet",
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Bruk nettadresse",
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Vis",
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Kun lyd",
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Multi-nedlastingsmodus",
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Last ned",
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Avbryt",
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Avansert",
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Simulert kommando:",
"4e4c721129466be9c3862294dc40241b64045998": "Bruk egendefinerte argumenter:",
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Egendefinerte argumenter",
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Du trenger ikke å inkludere nettadressen, kun alt etter. Argumenter skilles ved bruk av to komma, slik: ,,",
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Bruk defendefinert utdata",
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Egendefinert utdata",
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Dokumentasjon",
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "Sti er relativ til oppsettsnedlastingsstien. Ikke inkluder utvidelse.",
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Bruk identitetsbekreftelse",
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Brukernavn",
"c32ef07f8803a223a83ed17024b38e8d82292407": "Passord",
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Navn:",
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "Nettadresse:",
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Opplaster:",
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Filstørrelse:",
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Sti:",
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Opplastingsdato:",
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Lukk",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Endre spilleliste",
"511b600ae4cf037e4eb3b7a58410842cd5727490": "Legg til mer innhold",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Lagre",
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Antall:",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Rediger",
"826b25211922a1b46436589233cb6f1a163d89b7": "Slett",
"321e4419a943044e674beb55b8039f42a9761ca5": "Info",
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Slett og svartelist",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Last opp nye kaker",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Dra og slipp",
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "Merk: Oppasting av nye kaker overskriver tidligere. Merk deg også at kaker gjelder for hele instansen, ikke per bruker.",
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Innstillinger",
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "Nettadresse",
"54c512cca1923ab72faf1a0bd98d3d172469629a": "Nettadressen dette programmet nås fra, uten porten.",
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Port",
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Ønsket port. Forvalet er 17442.",
"d4477669a560750d2064051a510ef4d7679e2f3e": "Multi-brukermodus",
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Bruker-basissti",
"a64505c41150663968e277ec9b3ddaa5f4838798": "Basissti for brukere og deres nedlastede videoer.",
"4e3120311801c4acd18de7146add2ee4a4417773": "Tillat abonnementer",
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Abonnements-basissti",
"bc9892814ee2d119ae94378c905ea440a249b84a": "Basissti for videoer fra dine abonnementskanaler og spillelister. Den er relativ til YTDL-Material sin rotmappe.",
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Sjekkintervall",
"0f56a7449b77630c114615395bbda4cab398efd8": "I sekunder, kun tall.",
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Drakt",
"ff7cee38a2259526c519f878e71b964f41db4348": "Forvalg",
"adb4562d2dbd3584370e44496969d58c511ecb63": "Mørk",
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Tillat draktendring",
"fe46ccaae902ce974e2441abe752399288298619": "Språk",
"82421c3e46a0453a70c42900eab51d58d79e6599": "Generelt",
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Lydmappe",
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Sti for lydbaserte nedlastinger. Den er relativ til YTDL-Material sin rotmappe.",
"46826331da1949bd6fb74624447057099c9d20cd": "Videomappe",
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Sti for videonedlastinger. Den er relativ til YTDL-Material sin rotmappe.",
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Egendefinerte argumenter for nedlastninger på hjemmesiden for hele programmet. Argumenter skilles med to komma, slik: ,,",
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Bruk youtube-dl-arktivet",
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Inkluder miniatyrbilde",
"384de8f8f112c9e6092eb2698706d391553f3e8d": "Inkluder metadata",
"fb35145bfb84521e21b6385363d59221f436a573": "Drep alle nedlastinger",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Nedlaster",
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Topptittel",
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Filbehandler påskrudd",
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Nedlastingsbehandler påskrudd",
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Tillat kvalitetsvalg",
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Modus kun for nedlasting",
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Tillat multi-nedlastingsmodus",
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Skru på offentlig API",
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Offentlig API-nøkkel",
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Vis dokumentasjon",
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Generer",
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Dette vil slette din gamle API-nøkkel!",
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Bruk YouTube-API",
"ce10d31febb3d9d60c160750570310f303a22c22": "YouTube-API-nøkkel",
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Å generere en nøkkel er lett!",
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Klikk her",
"7f09776373995003161235c0c8d02b7f91dbc4df": "for å laste ned den offisielle Chrome-utvidelsen for YouTubeDL-Material selv.",
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Du må manuelt laste ned utvidelsen og endre dens innstillinger for å sette skjermflate-nettadresse.",
"9a2ec6da48771128384887525bdcac992632c863": "for å installere den offisielle Firefox-utvidelsen for YouTubeDL-Material rett fra Firefox sin utvidelsesside.",
"eb81be6b49e195e5307811d1d08a19259d411f37": "Detaljert oppsettsinstruks",
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Ikke mye kreves annet enn å endre utvidelses innstillinger for å sette skjermflate-nettadresse.",
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Dra lenken nedenfor til bokmerker. Naviger til YouTube-videoen du ønsker å laste ned og klikk på bokmerket.",
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Genrer \"kun lyd\"-bookmerke",
"d5f69691f9f05711633128b5a3db696783266b58": "Ekstra",
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Bruk forvalgt nedlastingsagent",
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Velg en nedlaster",
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Loggingsnivå",
"db6c192032f4cab809aad35215f0aa4765761897": "Innloggingsutløp",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Tillat avansert nedlasting",
"431e5f3a0dde88768d1074baedd65266412b3f02": "Bruk kaker",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Sett kaker",
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avansert",
"37224420db54d4bc7696f157b779a7225f03ca9d": "Tillat brukerregistrering",
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Intern",
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "Autentiseringsmetode",
"1db9789b93069861019bd0ccaa5d4706b00afc61": "LDAP-nettadresse",
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "BIND-DN",
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "BIND-identitetsdetaljer",
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Søkebase",
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Søkefilter",
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Brukere",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Logger",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Close} false {Cancel} other {otha} }",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Om YouTubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "er en fri YouTube-nedlaster bygd i henhold til Google sine Materielle spesifikasjoner. Du kan sømløst laste ned dine favorittvideoer som video- eller lydfiler, og tilogmed abonnere på dine favorittkanaler og spillelister for å holde deg oppdatert med nye videoer.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "har noen flotte funksjoner inkludert. Et vidtfavnende API, Docker-støtte, og lokalisering (oversettelser)-støtte. Les om alle støttede funksjoner ved å klikke på GitHub-ikonet ovenfor.",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Installert versjon:",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Ser etter oppdateringer …",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Oppdatering tilgjengelig",
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Du kan oppdatere fra innstillingsmenyen.",
"b33536f59b94ec935a16bd6869d836895dc5300c": "Funnet en feil eller har et forslag å komme med?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "for å opprette en feilrapport.",
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Din profil",
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Opprettet:",
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Du er ikke innlogget.",
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Logg inn",
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Logg ut",
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Opprett administratorkonto",
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Fant ingen administratorkonto. Dette vil opprette og sette passord for en slik konto med brukernavn som «admin».",
"70a67e04629f6d412db0a12d51820b480788d795": "Opprett",
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profil",
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Om",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Hjem",
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnementer",
"822fab38216f64e8166d368b59fe756ca39d301b": "Nedlastinger",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Del spilleliste",
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Del video",
"1d540dcd271b316545d070f9d182c372d923aadd": "Del lyd",
"1f6d14a780a37a97899dc611881e6bc971268285": "Skru på deling",
"6580b6a950d952df847cb3d8e7176720a740adc8": "Bruk tidsstempel",
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Sekunder",
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Kopier til utklippstavle",
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Lagre endringer",
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Nedlastet",
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "En feil inntraff",
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Detaljer",
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "En feil har inntruffet:",
"77b0c73840665945b25bd128709aa64c8f017e1c": "Nedlastingsstart:",
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Nedlastingsslutt:",
"ad127117f9471612f47d01eae09709da444a36a4": "Filsti(er):",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonner på en spilleliste eller kanal",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "Spilleliste- eller kanal-nettadressen",
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Egendefinert navn",
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Last ned alle opplastinger",
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Last ned videoer oppdatert siste",
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Kun lyd-modus",
"408ca4911457e84a348cecf214f02c69289aa8f1": "Kun strømming-modus",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Disse legges til etter standard-argumentene.",
"98b6ec9ec138186d663e64770267b67334353d63": "Egendefinert filutdata",
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Abonner",
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Type:",
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Arkiv:",
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Eksporter arkiv",
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Opphev abonnement",
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Dine abonnementer",
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanaler",
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Navn ikke tilgjengelig. Henter kanal …",
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Du har ingen kanalabonnementer.",
"47546e45bbb476baaaad38244db444c427ddc502": "Spillelister",
"2e0a410652cb07d069f576b61eab32586a18320d": "Navn ikke tilgjengelig. Henter spilleliste …",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Du har ingen spillelisteabonnement.",
"3697f8583ea42868aa269489ad366103d94aece7": "Redigering",
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Søk",
"2054791b822475aeaea95c0119113de3200f5e1c": "Lengde:",
"94e01842dcee90531caa52e4147f70679bac87fe": "Slett og last ned igjen",
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Slett for alltid",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Oppdaterer",
"1372e61c5bd06100844bd43b98b016aabc468f62": "Velg en versjon:",
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Regustrer",
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Økt-ID:",
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(nåværende)",
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Tøm alle nedlastinger",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Ingen nedlastninger tilgjengelige!",
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registrer en bruker",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Brukernavn",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Håndter bruker",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Bruker-UID:",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nytt passord",
"6498fa1b8f563988f769654a75411bb8060134b9": "Sett nytt passord",
"544e09cdc99a8978f48521d45f62db0da6dcf742": "Bruk rolle-forvalg",
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Ja",
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Nei",
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Håndter rolle",
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Brukernavn",
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Handlinger",
"632e8b20c98e8eec4059a605a4b011bb476137af": "Rediger bruker",
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Slett bruker",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Legg til brukere",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rediger rolle",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Linjer:",
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Tøm logger",
"ccf5ea825526ac490974336cb5c24352886abc07": "Åpne fil",
"5656a06f17c24b2d7eae9c221567b209743829a9": "Åpne fil i ny fane",
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Gå til abonnement",
"d02888c485d3aeab6de628508f4a00312a722894": "Mine videoer"
}

View File

@@ -0,0 +1,242 @@
{
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "创建播放列表",
"cff1428d10d59d14e45edec3c735a27b5482db59": "名称",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "音频文件",
"a52dae09be10ca3a65da918533ced3d3f4992238": "视频文件",
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "修改youtube-dl参数",
"7fc1946abe2b40f60059c6cd19975d677095fd19": "模拟新参数",
"0b71824ae71972f236039bed43f8d2323e8fd570": "添加参数",
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "按类别搜索",
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "使用参数值",
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "参数值",
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "添加参数",
"d7b35c384aecd25a516200d6921836374613dfe7": "取消",
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "修改",
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "Youtube下载器",
"a38ae1082fec79ba1f379978337385a539a28e73": "质量",
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "使用URL",
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "查看",
"4a9889d36910edc8323d7bab60858ab3da6d91df": "仅音频",
"96a01fafe135afc58b0f8071a4ab00234495ce18": "多下载模式",
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "下载",
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "取消",
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "高级",
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "模拟命令:",
"4e4c721129466be9c3862294dc40241b64045998": "使用自定义参数",
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "自定义参数",
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "不必指定URL仅需指定其后的部分。参数用两个逗号分隔,,",
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "使用自定义输出",
"d9c02face477f2f9cdaae318ccee5f89856851fb": "自定义输出",
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "文档",
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "该路径是相对于配置下载路径的,省略文件扩展名",
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "使用身份验证",
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "用户名",
"c32ef07f8803a223a83ed17024b38e8d82292407": "密码",
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "音频",
"9779715ac05308973d8f1c8658b29b986e92450f": "您的音频文件在这里",
"47546e45bbb476baaaad38244db444c427ddc502": "播放列表",
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "没有可用的播放列表。 通过单击蓝色加号按钮从您下载的音频文件创建一个。",
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "视频",
"960582a8b9d7942716866ecfb7718309728f2916": "您的视频文件在这里",
"0f59c46ca29e9725898093c9ea6b586730d0624e": "没有可用的播放列表。 通过单击蓝色加号按钮,从下载的视频文件中创建一个。",
"616e206cb4f25bd5885fc35925365e43cf5fb929": "名称:",
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL",
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "上传者:",
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "文件大小:",
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "路径:",
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "上传日期:",
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "关闭",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "修改播放列表",
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID",
"e684046d73bcee88e82f7ff01e2852789a05fc32": "数量:",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "编辑",
"826b25211922a1b46436589233cb6f1a163d89b7": "删除",
"321e4419a943044e674beb55b8039f42a9761ca5": "详情",
"34504b488c24c27e68089be549f0eeae6ebaf30b": "删除并拉黑",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "上传新Cookies",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "拖放",
"85e0725c870b28458fd3bbba905397d890f00a69": "注意加载新的Cookies将覆盖您以前的Cookie。并且Cookies的范围是整个实例而不是每个用户单独分开的。",
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "设置",
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
"54c512cca1923ab72faf1a0bd98d3d172469629a": "设置访问URL无需端口。",
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "端口",
"22e8f1d0423a3b784fe40fab187b92c06541b577": "设置目标端口。默认为17442。",
"d4477669a560750d2064051a510ef4d7679e2f3e": "多用户模式",
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "用户文件路径",
"a64505c41150663968e277ec9b3ddaa5f4838798": "用户及其下载视频的文件路径。",
"cbe16a57be414e84b6a68309d08fad894df797d6": "使用加密SSL",
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "证书文件路径",
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "密钥文件路径",
"4e3120311801c4acd18de7146add2ee4a4417773": "允许订阅",
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "订阅文件路径",
"bc9892814ee2d119ae94378c905ea440a249b84a": "订阅频道和播放列表中视频的文件路径(相对于根文件夹而言)。",
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "检查间隔",
"0f56a7449b77630c114615395bbda4cab398efd8": "单位是秒,只包含数字。",
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "使用youtube-dl存档",
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "根据youtube-dl的存档功能",
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "从您的订阅下载的视频会记录在订阅存档子目录中的文本文件中。",
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "这样一来,您无需取消订阅便可以从订阅中永久删除视频。并且它还可以在数据丢失的情况下记录已经下载了哪些视频。",
"27a56aad79d8b61269ed303f11664cc78bcc2522": "主题",
"ff7cee38a2259526c519f878e71b964f41db4348": "默认",
"adb4562d2dbd3584370e44496969d58c511ecb63": "暗黑",
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "允许更改主题",
"fe46ccaae902ce974e2441abe752399288298619": "语言",
"82421c3e46a0453a70c42900eab51d58d79e6599": "常规",
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "音频文件夹路径",
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "音频下载的文件路径。相对于YTDL-Material的根文件夹。",
"46826331da1949bd6fb74624447057099c9d20cd": "视频文件夹路径",
"17c92e6d47a213fa95b5aa344b3f258147123f93": "视频下载的文件路径。相对于YTDL-Material的根文件夹。",
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "开始页面上用于下载的全局自定义参数。参数由两个逗号分隔:,,",
"d01715b75228878a773ae6d059acc639d4898a03": "安全下载覆盖",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "下载程序",
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "首页标题",
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "启用文件管理",
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "启用下载管理",
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "允许选择下载质量",
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "仅下载模式",
"09d31c803a7252658694e1e3176b97f5655a3fe3": "开启多下载模式",
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "使用PIN码保护设置",
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "设置新PIN码",
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "启用公共API",
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "公共API密钥",
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "查看文档",
"1b258b258b4cc475ceb2871305b61756b0134f4a": "生成",
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "使用YouTube API",
"ce10d31febb3d9d60c160750570310f303a22c22": "Youtube API密钥",
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "生成密钥很简单!",
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "点击这里",
"7f09776373995003161235c0c8d02b7f91dbc4df": "来手动下载官方的YoutubeDL-Material Chrome扩展程序。",
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "您必须手动安装扩展并且在扩展的设置中输入下载器URL。",
"9a2ec6da48771128384887525bdcac992632c863": "直接从Firefox扩展商店安装官方的YoutubeDL-Material Firefox扩展程序。",
"eb81be6b49e195e5307811d1d08a19259d411f37": "详细的扩展说明。",
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "只需在扩展的设置中输入前端URL。",
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "只需将下面的链接拖放到书签栏中。在YouTube页面上您只需单击书签即可下载视频。",
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "生成“仅音频”书签",
"d5f69691f9f05711633128b5a3db696783266b58": "额外",
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "使用默认下载代理",
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "选择下载器",
"00e274c496b094a019f0679c3fab3945793f3335": "选择日志级别",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "开启高级下载选项",
"431e5f3a0dde88768d1074baedd65266412b3f02": "使用Cookies",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "设置Cookies",
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "高级",
"37224420db54d4bc7696f157b779a7225f03ca9d": "允许用户注册",
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "用户",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "日志",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "保存",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {关} false {取消} }",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "关于 YoutubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "是根据Google的Material Design规范构建的开源YouTube下载器。您可以将喜欢的视频下载为视频或音频文件并且可以订阅喜欢的频道和播放列表以便及时下载他们的新视频。",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "包含很多很棒的功能支持APIDocker和本地化。在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": "取消所有下载"
}