Compare commits

..

1 Commits

Author SHA1 Message Date
Isaac Abadi
1f143d449b Fixed bug where blacklists wouldn't work with multi-user mode 2020-09-24 01:37:48 -04:00
59 changed files with 1958 additions and 3616 deletions

View File

@@ -1,92 +0,0 @@
name: continuous integration
on:
push:
branches: [master, feat/*]
tags:
- v*
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: setup node
uses: actions/setup-node@v1
- name: install dependencies
run: |
npm install
cd backend
npm install
sudo npm install -g @angular/cli
- name: build
run: ng build --prod
- name: prepare artifact upload
shell: pwsh
run: |
New-Item -Name build -ItemType Directory
New-Item -Path build -Name youtubedl-material -ItemType Directory
Copy-Item -Path ./backend/appdata -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/audio -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/authentication -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/public -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/subscriptions -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/video -Recurse -Destination ./build/youtubedl-material
New-Item -Path ./build/youtubedl-material -Name users
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
- name: upload build artifact
uses: actions/upload-artifact@v1
with:
name: youtubedl-material
path: build
release:
runs-on: ubuntu-latest
needs: build
if: contains(github.ref, '/tags/v')
steps:
- name: checkout code
uses: actions/checkout@v2
- name: create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: YoutubeDL-Material ${{ github.ref }}
body: |
# New features
# Minor additions
# Bug fixes
draft: true
prerelease: false
- name: download build artifact
uses: actions/download-artifact@v1
with:
name: youtubedl-material
path: ${{runner.temp}}/youtubedl-material
- name: prepare release asset
shell: pwsh
run: Compress-Archive -Path ${{runner.temp}}/youtubedl-material -DestinationPath youtubedl-material-${{ github.ref }}.zip
- name: upload build asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./youtubedl-material-${{ github.ref }}.zip
asset_name: youtubedl-material-${{ github.ref }}.zip
asset_content_type: application/zip
- name: upload docker-compose asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./docker-compose.yml
asset_name: docker-compose.yml
asset_content_type: text/plain

View File

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

View File

@@ -1,29 +0,0 @@
name: docker
on:
push:
branches: [master]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: setup platform emulator
uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: build & push images
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8
push: true
tags: tzahi12345/youtubedl-material:nightly

View File

@@ -1,10 +1,10 @@
# YoutubeDL-Material
[![](https://img.shields.io/docker/pulls/tzahi12345/youtubedl-material.svg)](https://hub.docker.com/r/tzahi12345/youtubedl-material)
[![](https://img.shields.io/docker/image-size/tzahi12345/youtubedl-material?sort=date)](https://hub.docker.com/r/tzahi12345/youtubedl-material)
[![](https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg)](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
[![](https://img.shields.io/github/issues/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
[![](https://img.shields.io/github/license/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
[![Docker pulls badge](https://img.shields.io/docker/pulls/tzahi12345/youtubedl-material.svg)](https://hub.docker.com/r/tzahi12345/youtubedl-material)
[![Docker image size badge](https://img.shields.io/docker/image-size/tzahi12345/youtubedl-material?sort=date)](https://hub.docker.com/r/tzahi12345/youtubedl-material)
[![Heroku deploy badge](https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg)](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
[![GitHub issues badge](https://img.shields.io/github/issues/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
[![License badge](https://img.shields.io/github/license/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 9](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
@@ -30,25 +30,13 @@ Dark mode:
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
Debian/Ubuntu:
Make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
```bash
sudo apt-get install nodejs youtube-dl ffmpeg
```
CentOS 7:
```bash
sudo yum install epel-release
sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
sudo yum install centos-release-scl-rh
sudo yum install rh-nodejs12
scl enable rh-nodejs12 bash
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
sudo apt-get install nodejs youtube-dl
```
Optional dependencies:
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
### Installing
@@ -87,16 +75,14 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest Docker Compose, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
2. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17443" or something similar. This tells you the *container-internal* port of the application. Please check your `docker-compose.yml` file for the *external* port. If you downloaded the file as described above, it defaults to **8998**.
4. Make sure you can connect to the specified URL + *external* port, and if so, you are done!
NOTE: It is currently recommended that you use the `nightly` tag on Docker. To do so, simply update the docker-compose.yml `image` field so that it points to `tzahi12345/youtubedl-material:nightly`.
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
4. Make sure you can connect to the specified URL + port, and if so, you are done!
### Custom UID/GID
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
```yml
```
environment:
UID: YOUR_UID
GID: YOUR_GID
@@ -123,10 +109,8 @@ If you're interested in translating the app into a new language, check out the [
* **Isaac Grynsztein** (me!) - *Initial work*
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.

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,6 @@
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"default_file_output": "",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false,
@@ -26,10 +25,7 @@
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_API_key": "",
"twitch_auto_download_chat": false
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
@@ -53,7 +49,6 @@
}
},
"Advanced": {
"default_downloader": "youtube-dl",
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"multi_user_mode": false,

View File

@@ -139,12 +139,12 @@ exports.registerUser = function(req, res) {
exports.passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password'},
async function(username, password, done) {
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, (await bcrypt.compare(password, user.passhash)) ? user : false);
return done(null, bcrypt.compareSync(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,13 +226,15 @@ exports.ensureAuthenticatedElseError = function(req, res, next) {
// change password
exports.changeUserPassword = async function(user_uid, new_pass) {
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;
}
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);
});
});
}
// change user permissions
@@ -281,7 +283,6 @@ 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) {
@@ -295,7 +296,7 @@ exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
// prevent unauthorized users from accessing the file info
if (file && !file['sharingEnabled'] && requireSharing) file = null;
if (requireSharing && !file['sharingEnabled']) file = null;
return file;
}
@@ -350,7 +351,7 @@ exports.registerUserFile = function(user_uid, file_object, type) {
.write();
}
exports.deleteUserFile = async function(user_uid, file_uid, type, blacklistMode = false) {
exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) {
let success = false;
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
if (file_obj) {
@@ -373,20 +374,20 @@ exports.deleteUserFile = async function(user_uid, file_uid, type, blacklistMode
.remove({
uid: file_uid
}).write();
if (await fs.pathExists(full_path)) {
if (fs.existsSync(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 (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);
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);
}
await fs.unlink(full_path);
fs.unlinkSync(full_path);
// do archive stuff
@@ -395,17 +396,17 @@ exports.deleteUserFile = async function(user_uid, file_uid, type, blacklistMode
const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`);
// use subscriptions API to remove video from the archive file, and write it to the blacklist
if (await fs.pathExists(archive_path)) {
const line = youtube_id ? await subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
if (fs.existsSync(archive_path)) {
let line = youtube_id ? 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;
await fs.appendFile(blacklistPath, line);
fs.appendFileSync(blacklistPath, line);
}
} else {
logger.info(`Could not find archive file for ${type} files. Creating...`);
await fs.ensureFile(archive_path);
fs.ensureFileSync(archive_path);
}
}
}
@@ -531,7 +532,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

@@ -1,123 +0,0 @@
const config_api = require('./config');
var logger = null;
var db = null;
var users_db = null;
var db_api = null;
function setDB(input_db, input_users_db, input_db_api) { db = input_db; users_db = input_users_db; db_api = input_db_api }
function setLogger(input_logger) { logger = input_logger; }
function initialize(input_db, input_users_db, input_logger, input_db_api) {
setDB(input_db, input_users_db, input_db_api);
setLogger(input_logger);
}
/*
Categories:
Categories are a way to organize videos based on dynamic rules set by the user. Categories are universal (so not per-user).
Categories, besides rules, have an optional custom output. This custom output can help users create their
desired directory structure.
Rules:
A category rule consists of a property, a comparison, and a value. For example, "uploader includes 'VEVO'"
Rules are stored as an object with the above fields. In addition to those fields, it also has a preceding_operator, which
is either OR or AND, and signifies whether the rule should be ANDed with the previous rules, or just ORed. For the first
rule, this field is null.
Ex. (title includes 'Rihanna' OR title includes 'Beyonce' AND uploader includes 'VEVO')
*/
async function categorize(file_json) {
let selected_category = null;
const categories = getCategories();
if (!categories) {
logger.warn('Categories could not be found. Initializing categories...');
db.assign({categories: []}).write();
return null;
return;
}
for (let i = 0; i < categories.length; i++) {
const category = categories[i];
const rules = category['rules'];
// if rules for current category apply, then that is the selected category
if (applyCategoryRules(file_json, rules, category['name'])) {
selected_category = category;
logger.verbose(`Selected category ${category['name']} for ${file_json['webpage_url']}`);
return selected_category;
}
}
return selected_category;
}
function getCategories() {
const categories = db.get('categories').value();
return categories ? categories : null;
}
function applyCategoryRules(file_json, rules, category_name) {
let rules_apply = false;
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
let rule_applies = null;
let preceding_operator = rule['preceding_operator'];
switch (rule['comparator']) {
case 'includes':
rule_applies = file_json[rule['property']].includes(rule['value']);
break;
case 'not_includes':
rule_applies = !(file_json[rule['property']].includes(rule['value']));
break;
case 'equals':
rule_applies = file_json[rule['property']] === rule['value'];
break;
case 'not_equals':
rule_applies = file_json[rule['property']] !== rule['value'];
break;
default:
logger.warn(`Invalid comparison used for category ${category_name}`)
break;
}
// OR the first rule with rules_apply, which will be initially false
if (i === 0) preceding_operator = 'or';
// update rules_apply based on current rule
if (preceding_operator === 'or')
rules_apply = rules_apply || rule_applies;
else
rules_apply = rules_apply && rule_applies;
}
return rules_apply;
}
async function addTagToVideo(tag, video, user_uid) {
// TODO: Implement
}
async function removeTagFromVideo(tag, video, user_uid) {
// TODO: Implement
}
// adds tag to list of existing tags (used for tag suggestions)
async function addTagToExistingTags(tag) {
const existing_tags = db.get('tags').value();
if (!existing_tags.includes(tag)) {
db.get('tags').push(tag).write();
}
}
module.exports = {
initialize: initialize,
categorize: categorize,
}

View File

@@ -184,7 +184,6 @@ DEFAULT_CONFIG = {
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"default_file_output": "",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false,
@@ -203,10 +202,7 @@ DEFAULT_CONFIG = {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_API_key": "",
"twitch_auto_download_chat": false
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
@@ -230,7 +226,6 @@ DEFAULT_CONFIG = {
}
},
"Advanced": {
"default_downloader": "youtube-dl",
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"multi_user_mode": false,

View File

@@ -18,10 +18,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_video_folder_path',
'path': 'YoutubeDLMaterial.Downloader.path-video'
},
'ytdl_default_file_output': {
'key': 'ytdl_default_file_output',
'path': 'YoutubeDLMaterial.Downloader.default_file_output'
},
'ytdl_use_youtubedl_archive': {
'key': 'ytdl_use_youtubedl_archive',
'path': 'YoutubeDLMaterial.Downloader.use_youtubedl_archive'
@@ -86,18 +82,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_youtube_api_key',
'path': 'YoutubeDLMaterial.API.youtube_API_key'
},
'ytdl_use_twitch_api': {
'key': 'ytdl_use_twitch_api',
'path': 'YoutubeDLMaterial.API.use_twitch_API'
},
'ytdl_twitch_api_key': {
'key': 'ytdl_twitch_api_key',
'path': 'YoutubeDLMaterial.API.twitch_API_key'
},
'ytdl_twitch_auto_download_chat': {
'key': 'ytdl_twitch_auto_download_chat',
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
},
// Themes
'ytdl_default_theme': {
@@ -146,10 +130,6 @@ let CONFIG_ITEMS = {
},
// Advanced
'ytdl_default_downloader': {
'key': 'ytdl_default_downloader',
'path': 'YoutubeDLMaterial.Advanced.default_downloader'
},
'ytdl_use_default_downloading_agent': {
'key': 'ytdl_use_default_downloading_agent',
'path': 'YoutubeDLMaterial.Advanced.use_default_downloading_agent'

View File

@@ -15,10 +15,10 @@ function initialize(input_db, input_users_db, input_logger) {
setLogger(input_logger);
}
function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null) {
function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
let db_path = null;
const file_id = file_path.substring(0, file_path.length-4);
const file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub);
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path, sub);
if (!file_object) {
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
return false;
@@ -27,7 +27,7 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null, custo
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, customPath || multiUserMode && multiUserMode.file_path);
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, multiUserMode && multiUserMode.file_path);
if (!sub) {
if (multiUserMode) {
@@ -182,9 +182,9 @@ async function importUnregisteredFiles() {
}
// run through check list and check each file to see if it's missing from the db
for (const dir_to_check of dirs_to_check) {
dirs_to_check.forEach(dir_to_check => {
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
const files = utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
files.forEach(file => {
// check if file exists in db, if not add it
@@ -195,7 +195,7 @@ async function importUnregisteredFiles() {
logger.verbose(`Added discovered file to the database: ${file.id}`);
}
});
}
});
}

View File

@@ -79,18 +79,17 @@ async function getSubscriptionInfo(sub, user_uid = null) {
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
// 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 => {
// 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.');
}
}
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (debugMode) {
logger.info('Subscribe: got info for subscription ' + sub.id);
@@ -114,11 +113,7 @@ async function getSubscriptionInfo(sub, user_uid = null) {
continue;
}
if (!sub.name) {
if (sub.isPlaylist) {
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
} else {
sub.name = output_json.uploader;
}
sub.name = sub.isPlaylist ? output_json.playlist_title : output_json.uploader;
// if it's now valid, update
if (sub.name) {
if (user_uid)
@@ -157,36 +152,39 @@ async function getSubscriptionInfo(sub, user_uid = null) {
}
async function unsubscribe(sub, deleteMode, user_uid = null) {
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: '' };
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 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;
}
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);
}
await fs.rmdir(sub.archive);
// failed subs have no name, on unsubscribe they shouldn't error
if (!sub.name) {
return;
}
await fs.remove(appendedBasePath);
}
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);
}
deleteFolderRecursive(appendedBasePath);
}
});
}
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
@@ -204,159 +202,159 @@ 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');
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);
const [jsonExists, videoFileExists, imageFileExists, altImageFileExists] = await Promise.all([
fs.pathExists(jsonPath),
fs.pathExists(videoFilePath),
fs.pathExists(imageFilePath),
fs.pathExists(altImageFilePath),
]);
if (jsonExists) {
retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id'];
await fs.unlink(jsonPath);
}
if (imageFileExists) {
await fs.unlink(imageFilePath);
}
if (altImageFileExists) {
await fs.unlink(altImageFilePath);
}
if (videoFileExists) {
await fs.unlink(videoFilePath);
if ((await fs.pathExists(jsonPath)) || (await fs.pathExists(videoFilePath))) {
return 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 (await fs.pathExists(archive_path)) {
await removeIDFromArchive(archive_path, retrievedID);
}
}
return true;
if (jsonExists) {
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
fs.unlinkSync(jsonPath);
}
} else {
// TODO: tell user that the file didn't exist
return true;
}
if (imageFileExists) {
fs.unlinkSync(imageFilePath);
}
if (altImageFileExists) {
fs.unlinkSync(altImageFilePath);
}
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);
}
});
} else {
// TODO: tell user that the file didn't exist
resolve(true);
}
});
}
async function getVideosForSub(sub, user_uid = null) {
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
}
}
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
let fullOutput = `${appendedBasePath}/%(title)s.%(ext)s`;
if (sub.custom_output) {
fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`;
}
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
let qualityPath = null;
if (sub.type && sub.type === 'audio') {
qualityPath = ['-f', 'bestaudio']
qualityPath.push('-x');
qualityPath.push('--audio-format', 'mp3');
} else {
if (!sub.maxQuality || sub.maxQuality === 'best') qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'];
else qualityPath = ['-f', `bestvideo[height<=${sub.maxQuality}]+bestaudio/best[height<=${sub.maxQuality}]`, '--merge-output-format', 'mp4'];
}
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);
}
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 (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
// get videos
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
return new Promise(resolve => {
if (!subExists(sub.id, user_uid)) {
resolve(false);
return;
}
// 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
}
}
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
let fullOutput = appendedBasePath + '/%(title)s' + ext;
if (sub.custom_output) {
fullOutput = appendedBasePath + '/' + sub.custom_output + ext;
}
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
let qualityPath = null;
if (sub.type && sub.type === 'audio') {
qualityPath = ['-f', 'bestaudio']
qualityPath.push('-x');
qualityPath.push('--audio-format', 'mp3');
} else {
qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']
}
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);
}
let archive_dir = null;
let archive_path = null;
if (useArchive) {
if (sub.archive) {
archive_dir = sub.archive;
archive_path = path.join(archive_dir, 'archive.txt')
}
downloadConfig.push('--download-archive', archive_path);
}
// if streaming only mode, just get the list of videos
if (sub.streamingOnly) {
downloadConfig = ['-f', 'best', '--dump-json'];
}
if (sub.timerange) {
downloadConfig.push('--dateafter', sub.timerange);
}
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
// get videos
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
logger.verbose('Subscription: finished check for ' + sub.name);
if (err && !output) {
logger.error(err.stderr ? err.stderr : err.message);
logger.error(err.stderr);
if (err.stderr.includes('This video is unavailable')) {
logger.info('An error was encountered with at least one video, backup method will be used.')
try {
@@ -435,13 +433,6 @@ function getSubscription(subID, user_uid = null) {
return db.get('subscriptions').find({id: subID}).value();
}
function getSubscriptionByName(subName, user_uid = null) {
if (user_uid)
return users_db.get('users').find({uid: user_uid}).get('subscriptions').find({name: subName}).value();
else
return db.get('subscriptions').find({name: subName}).value();
}
function updateSubscription(sub, user_uid = null) {
if (user_uid) {
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign(sub).write();
@@ -465,8 +456,23 @@ function getAppendedBasePath(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
}
async function removeIDFromArchive(archive_path, id) {
let data = await fs.readFile(archive_path, {encoding: 'utf-8'});
// 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'});
if (!data) {
logger.error('Archive could not be found.');
return;
@@ -487,14 +493,13 @@ async function removeIDFromArchive(archive_path, id) {
// UPDATE FILE WITH NEW DATA
const updatedData = dataArray.join('\n');
await fs.writeFile(archive_path, updatedData);
fs.writeFileSync(archive_path, updatedData);
if (line) return line;
if (err) throw err;
}
module.exports = {
getSubscription : getSubscription,
getSubscriptionByName : getSubscriptionByName,
getAllSubscriptions : getAllSubscriptions,
updateSubscription : updateSubscription,
subscribe : subscribe,

View File

@@ -1,71 +0,0 @@
var moment = require('moment');
var Axios = require('axios');
async function getCommentsForVOD(clientID, vodId) {
let url = `https://api.twitch.tv/v5/videos/${vodId}/comments?content_offset_seconds=0`,
batch,
cursor;
let comments = null;
try {
do {
batch = (await Axios.get(url, {
headers: {
'Client-ID': clientID,
Accept: 'application/vnd.twitchtv.v5+json; charset=UTF-8',
'Content-Type': 'application/json; charset=UTF-8',
}
})).data;
const str = batch.comments.map(c => {
let {
created_at: msgCreated,
content_offset_seconds: timestamp,
commenter: {
name,
_id,
created_at: acctCreated
},
message: {
body: msg
}
} = c;
const timestamp_str = moment.duration(timestamp, 'seconds')
.toISOString()
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
(_, ...ms) => {
const seg = v => v ? v.padStart(2, '0') : '00';
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
});
acctCreated = moment(acctCreated).utc();
msgCreated = moment(msgCreated).utc();
if (!comments) comments = [];
comments.push({
timestamp: timestamp,
timestamp_str: timestamp_str,
name: name,
message: msg
});
// let line = `${timestamp},${msgCreated.format(tsFormat)},${name},${_id},"${msg.replace(/"/g, '""')}",${acctCreated.format(tsFormat)}`;
// return line;
}).join('\n');
cursor = batch._next;
url = `https://api.twitch.tv/v5/videos/${vodId}/comments?cursor=${cursor}`;
await new Promise(res => setTimeout(res, 300));
} while (cursor);
} catch (err) {
console.error(err);
}
return comments;
}
module.exports = {
getCommentsForVOD: getCommentsForVOD
}

View File

@@ -4,7 +4,6 @@ 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;
@@ -20,21 +19,21 @@ function getTrueFileName(unfixed_path, type) {
return fixed_path;
}
async function getDownloadedFilesByType(basePath, type) {
function getDownloadedFilesByType(basePath, type) {
// return empty array if the path doesn't exist
if (!(await fs.pathExists(basePath))) return [];
if (!fs.existsSync(basePath)) return [];
let files = [];
const ext = type === 'audio' ? 'mp3' : 'mp4';
var located_files = await recFindByExt(basePath, ext);
var located_files = recFindByExt(basePath, ext);
for (let i = 0; i < located_files.length; i++) {
let file = located_files[i];
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
var stats = await fs.stat(file);
var stats = fs.statSync(file);
var id = file_path.substring(0, file_path.length-4);
var jsonobj = await getJSONByType(type, id, basePath);
var jsonobj = getJSONByType(type, id, basePath);
if (!jsonobj) continue;
var title = jsonobj.title;
var url = jsonobj.webpage_url;
@@ -114,7 +113,6 @@ function getExpectedFileSize(info_json) {
const formats = info_json['format_id'].split('+');
let expected_filesize = 0;
formats.forEach(format_id => {
if (!info_json.formats) return expected_filesize;
info_json.formats.forEach(available_format => {
if (available_format.format_id === format_id && available_format.filesize) {
expected_filesize += available_format.filesize;
@@ -131,7 +129,7 @@ function fixVideoMetadataPerms(name, type, customPath = null) {
: config_api.getConfigItem('ytdl_video_folder_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
const files_to_fix = [
// JSONs
path.join(customPath, name + '.info.json'),
@@ -160,25 +158,27 @@ function deleteJSONFile(name, type, customPath = null) {
}
async function recFindByExt(base,ext,files,result)
function recFindByExt(base,ext,files,result)
{
files = files || (await fs.readdir(base))
files = files || fs.readdirSync(base)
result = result || []
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 )
files.forEach(
function (file) {
var newbase = path.join(base,file)
if ( fs.statSync(newbase).isDirectory() )
{
result.push(newbase)
result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result)
}
else
{
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
{
result.push(newbase)
}
}
}
}
)
return result
}

View File

@@ -46,7 +46,7 @@
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<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)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar>{{subscription.name}}</a>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
</ng-container>
</mat-nav-list>
</mat-sidenav>

View File

@@ -116,8 +116,6 @@ export class AppComponent implements OnInit, AfterViewInit {
if (this.allowSubscriptions) {
this.postsService.reloadSubscriptions();
}
this.postsService.reloadCategories();
}
// theme stuff

View File

@@ -79,8 +79,6 @@ import { UnifiedFileCardComponent } from './components/unified-file-card/unified
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
import { CustomPlaylistsComponent } from './components/custom-playlists/custom-playlists.component';
import { EditCategoryDialogComponent } from './dialogs/edit-category-dialog/edit-category-dialog.component';
import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.component';
registerLocaleData(es, 'es');
@@ -125,9 +123,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
UnifiedFileCardComponent,
RecentVideosComponent,
EditSubscriptionDialogComponent,
CustomPlaylistsComponent,
EditCategoryDialogComponent,
TwitchChatComponent
CustomPlaylistsComponent
],
imports: [
CommonModule,

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" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
</div>
</div>
</div>

View File

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

View File

@@ -61,8 +61,7 @@ export class LogsViewerComponent implements OnInit {
data: {
dialogTitle: 'Clear logs',
dialogText: 'Would you like to clear your logs? This will delete all your current logs, permanently.',
submitText: 'Clear',
warnSubmitColor: true
submitText: 'Clear'
}
});
dialogRef.afterClosed().subscribe(confirmed => {

View File

@@ -32,12 +32,12 @@
<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 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)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? '?jwt=' + this.postsService.token : ''"></app-unified-file-card>
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
</div>
</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 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
</div>
</ng-container>
</div>

View File

@@ -210,7 +210,7 @@ export class RecentVideosComponent implements OnInit {
if (!this.postsService.config.Extra.file_manager_enabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(name, type).subscribe(delRes => {
this.postsService.deleteFile(name, false).subscribe(delRes => {
// reload mp4s
this.getAllFiles();
});
@@ -233,7 +233,7 @@ export class RecentVideosComponent implements OnInit {
}
deleteNormalFile(file, index, blacklistMode = false) {
this.postsService.deleteFile(file.uid, file.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
if (result) {
this.postsService.openSnackBar('Delete success!', 'OK.');
this.files.splice(index, 1);

View File

@@ -1,11 +0,0 @@
<div class="chat-container" #scrollContainer *ngIf="visible_chat">
<div style="width: 250px; text-align: center;"><strong>Twitch Chat</strong></div>
<div style="max-width: 250px" *ngFor="let chat of visible_chat">
{{chat.timestamp_str}} - <strong>{{chat.name}}</strong>: {{chat.message}}
</div>
</div>
<ng-container *ngIf="chat_response_received && !full_chat">
<button (click)="downloadTwitchChat()" class="download-button" mat-raised-button color="accent"><ng-container i18n="Download Twitch Chat button">Download Twitch Chat</ng-container></button>
<mat-spinner *ngIf="downloading_chat" class="downloading-spinner" [diameter]="30"></mat-spinner>
</ng-container>

View File

@@ -1,13 +0,0 @@
.chat-container {
height: 100%;
overflow-y: scroll;
}
.download-button {
margin: 10px;
}
.downloading-spinner {
top: 50%;
left: 80px;
}

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TwitchChatComponent } from './twitch-chat.component';
describe('TwitchChatComponent', () => {
let component: TwitchChatComponent;
let fixture: ComponentFixture<TwitchChatComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TwitchChatComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TwitchChatComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,135 +0,0 @@
import { AfterViewInit, Component, ElementRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-twitch-chat',
templateUrl: './twitch-chat.component.html',
styleUrls: ['./twitch-chat.component.scss']
})
export class TwitchChatComponent implements OnInit, AfterViewInit {
full_chat = null;
visible_chat = null;
chat_response_received = false;
downloading_chat = false;
current_chat_index = null;
CHAT_CHECK_INTERVAL_MS = 200;
chat_check_interval_obj = null;
scrollContainer = null;
@Input() db_file = null;
@Input() current_timestamp = null;
@ViewChild('scrollContainer') scrollRef: ElementRef;
constructor(private postsService: PostsService) { }
ngOnInit(): void {
this.getFullChat();
}
ngAfterViewInit() {
}
private isUserNearBottom(): boolean {
const threshold = 300;
const position = this.scrollContainer.scrollTop + this.scrollContainer.offsetHeight;
const height = this.scrollContainer.scrollHeight;
return position > height - threshold;
}
scrollToBottom = () => {
this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight;
}
addNewChatMessages() {
if (!this.scrollContainer) {
this.scrollContainer = this.scrollRef.nativeElement;
}
if (this.current_chat_index === null) {
this.current_chat_index = this.getIndexOfNextChat();
}
const latest_chat_timestamp = this.visible_chat.length ? this.visible_chat[this.visible_chat.length - 1]['timestamp'] : 0;
for (let i = this.current_chat_index + 1; i < this.full_chat.length; i++) {
if (this.full_chat[i]['timestamp'] >= latest_chat_timestamp && this.full_chat[i]['timestamp'] <= this.current_timestamp) {
this.visible_chat.push(this.full_chat[i]);
this.current_chat_index = i;
if (this.isUserNearBottom()) {
this.scrollToBottom();
}
} else if (this.full_chat[i]['timestamp'] > this.current_timestamp) {
break;
}
}
}
getIndexOfNextChat() {
const index = binarySearch(this.full_chat, 'timestamp', this.current_timestamp);
return index;
}
getFullChat() {
this.postsService.getFullTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', null).subscribe(res => {
this.chat_response_received = true;
if (res['chat']) {
this.initializeChatCheck(res['chat']);
}
});
}
renewChat() {
this.visible_chat = [];
this.current_chat_index = this.getIndexOfNextChat();
}
downloadTwitchChat() {
this.downloading_chat = true;
let vodId = this.db_file.url.split('videos/').length > 1 && this.db_file.url.split('videos/')[1];
vodId = vodId.split('?')[0];
if (!vodId) {
this.postsService.openSnackBar('VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"');
}
this.postsService.downloadTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', vodId, null).subscribe(res => {
if (res['chat']) {
this.initializeChatCheck(res['chat']);
} else {
this.downloading_chat = false;
this.postsService.openSnackBar('Download failed.')
}
}, err => {
this.downloading_chat = false;
this.postsService.openSnackBar('Chat could not be downloaded.')
});
}
initializeChatCheck(full_chat) {
this.full_chat = full_chat;
this.visible_chat = [];
this.chat_check_interval_obj = setInterval(() => this.addNewChatMessages(), this.CHAT_CHECK_INTERVAL_MS);
}
}
function binarySearch(arr, key, n) {
let min = 0;
let max = arr.length - 1;
let mid;
while (min <= max) {
// tslint:disable-next-line: no-bitwise
mid = (min + max) >>> 1;
if (arr[mid][key] === n) {
return mid;
} else if (arr[mid][key] < n) {
min = mid + 1;
} else {
max = mid - 1;
}
}
return min;
}

View File

@@ -1,19 +1,7 @@
<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 (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 *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>
@@ -41,7 +29,7 @@
<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.thumbnailPath ? thumbnailBlobURL : 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

@@ -1,22 +1,7 @@
import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { 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',
@@ -43,18 +28,11 @@ export class UnifiedFileCardComponent implements OnInit {
@Input() use_youtubedl_archive = false;
@Input() is_playlist = false;
@Input() index: number;
@Input() locale = null;
@Input() baseStreamPath = null;
@Input() jwtString = 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:
small: 150x175
@@ -69,12 +47,11 @@ export class UnifiedFileCardComponent implements OnInit {
this.file_length = fancyTimeFormat(this.file_obj.duration);
}
if (this.file_obj && this.file_obj.thumbnailPath) {
this.thumbnailBlobURL = `${this.baseStreamPath}thumbnail/${encodeURIComponent(this.file_obj.thumbnailPath)}${this.jwtString}`;
/*const mime = getMimeByFilename(this.file_obj.thumbnailPath);
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);*/
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);
}
}
@@ -110,15 +87,6 @@ export class UnifiedFileCardComponent implements OnInit {
});
}
onRightClick(event) {
event.preventDefault();
this.contextMenuPosition.x = event.clientX + 'px';
this.contextMenuPosition.y = event.clientY + 'px';
this.contextMenu.menuData = { 'item': {id: 1, name: 'hi'} };
this.contextMenu.menu.focusFirstItem('mouse');
this.contextMenu.openMenu();
}
}
function fancyTimeFormat(time) {

View File

@@ -6,7 +6,7 @@
</mat-dialog-content>
<mat-dialog-actions>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button [color]="warnSubmitColor ? 'warn' : 'primary'" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
<button color="primary" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
<div class="mat-spinner" *ngIf="submitClicked">
<mat-spinner [diameter]="25"></mat-spinner>
</div>

View File

@@ -15,14 +15,12 @@ export class ConfirmDialogComponent implements OnInit {
doneEmitter: EventEmitter<any> = null;
onlyEmitOnDone = false;
warnSubmitColor = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
if (this.data.dialogTitle) { this.dialogTitle = this.data.dialogTitle };
if (this.data.dialogText) { this.dialogText = this.data.dialogText };
if (this.data.submitText) { this.submitText = this.data.submitText };
if (this.data.warnSubmitColor) { this.warnSubmitColor = this.data.warnSubmitColor };
// checks if emitter exists, if so don't autoclose as it should be handled by caller
if (this.data.doneEmitter) {

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 override your previous cookies. Also note that cookies are instance-wide, not per-user.</p>
<p style="font-size: 14px;" i18n="Cookies upload warning">NOTE: Uploading new cookies will overrride your previous cookies. Also note that cookies are instance-wide, not per-user.</p>
</div>
<div style="margin-top: 10px;">
<table class="table">

View File

@@ -1,60 +0,0 @@
<h4 mat-dialog-title><ng-container i18n="Editing category dialog title">Editing category</ng-container>&nbsp;{{category['name']}}</h4>
<mat-dialog-content style="max-height: 50vh">
<mat-form-field style="width: 250px; margin-bottom: 5px;">
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="category['name']" required>
</mat-form-field>
<mat-divider></mat-divider>
<h6 style="margin-top: 20px;" i18n="Rules">Rules</h6>
<mat-list>
<mat-list-item *ngFor="let rule of category['rules']; let i = index">
<mat-form-field [style.visibility]="i === 0 ? 'hidden' : null" class="operator-select">
<mat-select [disabled]="i === 0" [(ngModel)]="rule['preceding_operator']">
<mat-option value="or">OR</mat-option>
<mat-option value="and">AND</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="property-select">
<mat-select [(ngModel)]="rule['property']">
<mat-option *ngFor="let propertyOption of propertyOptions" [value]="propertyOption.value">{{propertyOption.label}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="comparator-select">
<mat-select [(ngModel)]="rule['comparator']">
<mat-option *ngFor="let comparatorOption of comparatorOptions" [value]="comparatorOption.value">{{comparatorOption.label}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="value-input">
<input matInput [(ngModel)]="rule['value']">
</mat-form-field>
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
</mat-list-item>
</mat-list>
<button style="margin-bottom: 8px;" mat-icon-button (click)="addNewRule()" matTooltip="Add new rule" i18n-matTooltip="Add new rule tooltip"><mat-icon>add</mat-icon></button>
<mat-divider></mat-divider>
<mat-form-field style="width: 250px; margin-top: 10px;">
<input matInput [(ngModel)]="category['custom_output']" placeholder="Custom file output" i18n-placeholder="Category custom file output placeholder">
<mat-hint>
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Cancel">Cancel</ng-container></button>
<button mat-button [disabled]="categoryChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
<div class="mat-spinner" *ngIf="updating">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -1,16 +0,0 @@
.operator-select {
width: 55px;
}
.property-select {
margin-left: 10px;
width: 110px;
}
.comparator-select {
margin-left: 10px;
}
.value-input {
margin-left: 10px;
}

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { EditCategoryDialogComponent } from './edit-category-dialog.component';
describe('EditCategoryDialogComponent', () => {
let component: EditCategoryDialogComponent;
let fixture: ComponentFixture<EditCategoryDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ EditCategoryDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EditCategoryDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,111 +0,0 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-edit-category-dialog',
templateUrl: './edit-category-dialog.component.html',
styleUrls: ['./edit-category-dialog.component.scss']
})
export class EditCategoryDialogComponent implements OnInit {
updating = false;
original_category = null;
category = null;
propertyOptions = [
{
value: 'fulltitle',
label: 'Title'
},
{
value: 'id',
label: 'ID'
},
{
value: 'webpage_url',
label: 'URL'
},
{
value: 'view_count',
label: 'Views'
},
{
value: 'uploader',
label: 'Uploader'
},
{
value: '_filename',
label: 'File Name'
},
{
value: 'tags',
label: 'Tags'
}
];
comparatorOptions = [
{
value: 'includes',
label: 'includes'
},
{
value: 'not_includes',
label: 'not includes'
},
{
value: 'equals',
label: 'equals'
},
{
value: 'not_equals',
label: 'not equals'
},
];
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private postsService: PostsService) {
if (this.data) {
this.original_category = this.data.category;
this.category = JSON.parse(JSON.stringify(this.original_category));
}
}
ngOnInit(): void {
}
addNewRule() {
this.category['rules'].push({
preceding_operator: 'or',
property: 'fulltitle',
comparator: 'includes',
value: ''
});
}
saveClicked() {
this.updating = true;
this.postsService.updateCategory(this.category).subscribe(res => {
this.updating = false;
this.original_category = JSON.parse(JSON.stringify(this.category));
this.postsService.reloadCategories();
}, err => {
this.updating = false;
console.error(err);
});
}
categoryChanged() {
return JSON.stringify(this.category) === JSON.stringify(this.original_category);
}
swapRules(original_index, new_index) {
[this.category.rules[original_index], this.category.rules[new_index]] = [this.category.rules[new_index],
this.category.rules[original_index]];
}
removeRule(index) {
this.category['rules'].splice(index, 1);
}
}

View File

@@ -1,4 +1,4 @@
<h4 mat-dialog-title i18n="Edit subscription dialog title prefix">Editing</h4>&nbsp;{{sub.name}}
<h4 mat-dialog-title i18n="Edit subscription dialog title">Editing {{sub.name}}</h4>
<mat-dialog-content>
<div class="container-fluid">
@@ -24,13 +24,6 @@
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12 mt-2">
<mat-form-field>
<mat-select placeholder="Max quality" i18n-placeholder="Max quality placeholder" [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.maxQuality">
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<div>
<mat-checkbox [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.streamingOnly"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>

View File

@@ -22,36 +22,6 @@ export class EditSubscriptionDialogComponent implements OnInit {
audioOnlyMode = null;
download_all = null;
available_qualities = [
{
'label': 'Best',
'value': 'best'
},
{
'label': '4K',
'value': '2160'
},
{
'label': '1440p',
'value': '1440'
},
{
'label': '1080p',
'value': '1080'
},
{
'label': '720p',
'value': '720'
},
{
'label': '480p',
'value': '480'
},
{
'label': '360p',
'value': '360'
}
];
time_units = [
'day',
@@ -69,12 +39,16 @@ export class EditSubscriptionDialogComponent implements OnInit {
if (this.sub.timerange) {
const timerange_str = this.sub.timerange.split('-')[1];
console.log(timerange_str);
const number = timerange_str.replace(/\D/g,'');
let units = timerange_str.replace(/[0-9]/g, '');
if (+number === 1) {
units = units.replace('s', '');
}
console.log(units);
// // remove plural on units
// if (units[units.length-1] === 's') {
// units = units.substring(0, units.length-1);
// }
this.timerange_amount = parseInt(number);
this.timerange_unit = units;
@@ -97,10 +71,9 @@ export class EditSubscriptionDialogComponent implements OnInit {
}
saveSubscription() {
this.postsService.updateSubscription(this.new_sub).subscribe(res => {
this.postsService.updateSubscription(this.sub).subscribe(res => {
this.sub = this.new_sub;
this.new_sub = JSON.parse(JSON.stringify(this.sub));
this.postsService.reloadSubscriptions();
})
}
@@ -112,16 +85,12 @@ export class EditSubscriptionDialogComponent implements OnInit {
}
timerangeChanged(value, select_changed) {
if (+this.timerange_amount === 1) {
this.timerange_unit = this.timerange_unit.replace('s', '');
} else {
if (!this.timerange_unit.includes('s')) {
this.timerange_unit += 's';
}
}
console.log(this.timerange_amount);
console.log(this.timerange_unit);
if (this.timerange_amount && this.timerange_unit && !this.download_all) {
this.new_sub.timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
console.log(this.new_sub.timerange);
} else {
this.new_sub.timerange = null;
}

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"><ng-container i18n="Add more content">Add more content</ng-container></button>
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu">Add more content</button>
</div>
<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"><ng-container i18n="Save">Save</ng-container></button>
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent">Save</button>
</mat-dialog-actions>

View File

@@ -35,13 +35,6 @@
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-form-field>
<mat-select placeholder="Max quality" i18n-placeholder="Max quality placeholder" [disabled]="audioOnlyMode" [(ngModel)]="maxQuality">
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<div>
<mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>

View File

@@ -17,8 +17,6 @@ export class SubscribeDialogComponent implements OnInit {
url = null;
name = null;
maxQuality = 'best';
// state
subscribing = false;
@@ -31,43 +29,12 @@ export class SubscribeDialogComponent implements OnInit {
customFileOutput = '';
customArgs = '';
available_qualities = [
{
'label': 'Best',
'value': 'best'
},
{
'label': '4K',
'value': '2160'
},
{
'label': '1440p',
'value': '1440'
},
{
'label': '1080p',
'value': '1080'
},
{
'label': '720p',
'value': '720'
},
{
'label': '480p',
'value': '480'
},
{
'label': '360p',
'value': '360'
}
];
time_units = [
'day',
'week',
'month',
'year'
];
]
constructor(private postsService: PostsService,
private snackBar: MatSnackBar,
@@ -90,7 +57,7 @@ export class SubscribeDialogComponent implements OnInit {
if (!this.download_all) {
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
}
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode, this.maxQuality,
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode,
this.audioOnlyMode, this.customArgs, this.customFileOutput).subscribe(res => {
this.subscribing = false;
if (res['new_sub']) {

View File

@@ -56,7 +56,7 @@ export class FileCardComponent implements OnInit {
deleteFile(blacklistMode = false) {
if (!this.playlist) {
this.postsService.deleteFile(this.uid, this.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => {
if (result) {
this.openSnackBar('Delete success!', 'OK.');
this.removeFile.emit(this.name);

View File

@@ -182,7 +182,7 @@
</ng-template>
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
<app-recent-videos #recentVideos></app-recent-videos>
<app-recent-videos></app-recent-videos>
<br/>
<h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>

View File

@@ -20,7 +20,6 @@ import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.com
import { Platform } from '@angular/cdk/platform';
import { v4 as uuid } from 'uuid';
import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
import { RecentVideosComponent } from 'app/components/recent-videos/recent-videos.component';
export let audioFilesMouseHovering = false;
export let videoFilesMouseHovering = false;
@@ -201,7 +200,6 @@ export class MainComponent implements OnInit {
formats_loading = false;
@ViewChild('urlinput', { read: ElementRef }) urlInput: ElementRef;
@ViewChild('recentVideos') recentVideos: RecentVideosComponent;
@ViewChildren('audiofilecard') audioFileCards: QueryList<FileCardComponent>;
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
last_valid_url = '';
@@ -489,7 +487,6 @@ export class MainComponent implements OnInit {
this.downloadingfile = false;
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
// do nothing
this.reloadRecentVideos();
} else {
// if download only mode, just download the file. no redirect
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
@@ -499,7 +496,6 @@ export class MainComponent implements OnInit {
} else {
this.downloadAudioFile(decodeURI(name));
}
this.reloadRecentVideos();
} else {
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
if (is_playlist) {
@@ -528,7 +524,6 @@ export class MainComponent implements OnInit {
this.downloadingfile = false;
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
// do nothing
this.reloadRecentVideos();
} else {
// if download only mode, just download the file. no redirect
if (forceView === false && this.downloadOnlyMode) {
@@ -538,7 +533,6 @@ export class MainComponent implements OnInit {
} else {
this.downloadVideoFile(decodeURI(name));
}
this.reloadRecentVideos();
} else {
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
if (is_playlist) {
@@ -752,7 +746,7 @@ export class MainComponent implements OnInit {
if (!this.fileManagerEnabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(name, 'video').subscribe(delRes => {
this.postsService.deleteFile(name, true).subscribe(delRes => {
// reload mp3s
this.getMp3s();
});
@@ -769,7 +763,7 @@ export class MainComponent implements OnInit {
if (!this.fileManagerEnabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(name, 'audio').subscribe(delRes => {
this.postsService.deleteFile(name, false).subscribe(delRes => {
// reload mp4s
this.getMp4s();
});
@@ -1167,10 +1161,4 @@ export class MainComponent implements OnInit {
}
});
}
reloadRecentVideos() {
if (this.recentVideos) {
this.recentVideos.getAllFiles();
}
}
}

View File

@@ -1,46 +1,35 @@
<div style="height: 100%" *ngIf="playlist.length > 0 && show_player">
<div style="height: 100%" [ngClass]="(type === 'audio') ? null : 'container-video'">
<div style="max-width: 100%; margin-left: 0px; height: 100%">
<mat-drawer-container style="height: 100%" class="example-container" autosize>
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
</video>
</vg-player>
</div>
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
</mat-button-toggle-group>
</div>
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" opened="false">
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv/videos/')">
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime"></app-twitch-chat>
</ng-container>
</mat-drawer>
<div *ngIf="db_file && db_file.url.includes('twitch.tv/videos/') && postsService['config']['API']['use_twitch_API']" style="position: absolute; right: 0px">
<button style="right: 0px; top: -46px;" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
</div>
<div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
<div class="spinner-div">
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
</div>
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button><ng-container i18n="Playlist save changes button">Save changes</ng-container>&nbsp;<mat-icon>update</mat-icon></button>
</div>
<div *ngIf="playlist.length > 1">
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
<button *ngIf="!id" color="accent" class="favorite-button" color="primary" (click)="namePlaylistDialog()" mat-fab><mat-icon class="save-icon">favorite</mat-icon></button>
<button *ngIf="!is_shared && id && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
</div>
<div *ngIf="playlist.length === 1">
<button class="save-button" color="primary" (click)="downloadFile()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
<button *ngIf="!is_shared && uid && uid !== 'false' && type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
</div>
</mat-drawer-container>
<div *ngIf="playlist.length > 0 && show_player">
<div [ngClass]="(type === 'audio') ? null : 'container-video'">
<div style="max-width: 100%; margin-left: 0px; height: 70vh">
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
</video>
</vg-player>
</div>
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
</mat-button-toggle-group>
</div>
</div>
</div>
<div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
<div class="spinner-div">
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
</div>
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button><ng-container i18n="Playlist save changes button">Save changes</ng-container>&nbsp;<mat-icon>update</mat-icon></button>
</div>
<div *ngIf="playlist.length > 1">
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
<button *ngIf="!id" color="accent" class="favorite-button" color="primary" (click)="namePlaylistDialog()" mat-fab><mat-icon class="save-icon">favorite</mat-icon></button>
<button *ngIf="!is_shared && id && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
</div>
<div *ngIf="playlist.length === 1">
<button class="save-button" color="primary" (click)="downloadFile()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
<button *ngIf="!is_shared && uid && uid !== 'false' && type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit, ViewChild } 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';
@@ -7,7 +7,6 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component';
import { TwitchChatComponent } from 'app/components/twitch-chat/twitch-chat.component';
export interface IMedia {
title: string;
@@ -32,7 +31,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
currentIndex = 0;
currentItem: IMedia = null;
api: VgAPI;
api_ready = false;
// params
fileNames: string[];
@@ -67,8 +65,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
save_volume_timer = null;
original_volume = null;
@ViewChild('twitchchat') twitchChat: TwitchChatComponent;
@HostListener('window:resize', ['$event'])
onResize(event) {
this.innerWidth = window.innerWidth;
@@ -128,8 +124,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
this.getFile();
} else if (this.id) {
this.getPlaylistFiles();
} else if (this.subscriptionName) {
this.getSubscription();
}
if (this.url) {
@@ -145,7 +139,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
this.currentItem = this.playlist[0];
this.currentIndex = 0;
this.show_player = true;
} else if (this.fileNames && !this.subscriptionName) {
} else if (this.subscriptionName || this.fileNames) {
this.show_player = true;
this.parseFileNames();
}
@@ -177,25 +171,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
getSubscription() {
this.postsService.getSubscription(null, this.subscriptionName).subscribe(res => {
const subscription = res['subscription'];
if (this.fileNames) {
subscription.videos.forEach(video => {
if (video['id'] === this.fileNames[0]) {
this.db_file = video;
this.show_player = true;
this.parseFileNames();
}
});
} else {
console.log('no file name specified');
}
}, err => {
this.openSnackBar(`Failed to find subscription ${this.subscriptionName}`, 'Dismiss');
});
}
getPlaylistFiles() {
this.postsService.getPlaylist(this.id, null, this.uuid).subscribe(res => {
if (res['playlist']) {
@@ -227,26 +202,23 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
const fileName = this.fileNames[i];
let baseLocation = null;
let fullLocation = null;
if (!this.subscriptionName) {
baseLocation = this.type + '/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
} else {
// default to video but include subscription name param
baseLocation = this.type === 'audio' ? 'audio/' : 'video/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
'&subPlaylist=' + this.subPlaylist;
}
// 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.type || !this.db_file) ? `&type=${this.type}` : `&type=${this.db_file.type}`
const type_str = (this.id || !this.db_file) ? '' : `&type=${this.db_file.type}`
const id_str = this.id ? `&id=${this.id}` : '';
const file_path_str = (!this.db_file) ? '' : `&file_path=${encodeURIComponent(this.db_file.path)}`;
if (!this.subscriptionName) {
baseLocation = 'stream/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + `?test=test${type_str}${file_path_str}`;
} else {
// default to video but include subscription name param
baseLocation = 'stream/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
'&subPlaylist=' + this.subPlaylist + `${file_path_str}${type_str}`;
}
if (this.postsService.isLoggedIn) {
fullLocation += (this.subscriptionName ? '&' : '&') + `jwt=${this.postsService.token}`;
fullLocation += (this.subscriptionName ? '&' : '?') + `jwt=${this.postsService.token}`;
if (this.is_shared) { fullLocation += `${uuid_str}${uid_str}${type_str}${id_str}`; }
} else if (this.is_shared) {
fullLocation += (this.subscriptionName ? '&' : '?') + `test=test${uuid_str}${uid_str}${type_str}${id_str}`;
@@ -274,13 +246,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
onPlayerReady(api: VgAPI) {
this.api = api;
this.api_ready = true;
this.api.subscriptions.seeked.subscribe(data => {
if (this.twitchChat) {
this.twitchChat.renewChat();
}
});
// checks if volume has been previously set. if so, use that as default
if (localStorage.getItem('player_volume')) {
@@ -352,8 +317,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
this.downloading = true;
this.postsService.downloadFileFromServer(fileNames, this.type, zipName, null, null, null, null,
!this.uuid ? this.postsService.user.uid : this.uuid, this.id).subscribe(res => {
this.postsService.downloadFileFromServer(fileNames, this.type, zipName).subscribe(res => {
this.downloading = false;
const blob: Blob = res;
saveAs(blob, zipName + '.zip');

View File

@@ -11,8 +11,6 @@ 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';
import { Title } from '@angular/platform-browser';
@Injectable()
export class PostsService implements CanActivate {
@@ -54,12 +52,10 @@ export class PostsService implements CanActivate {
// global vars
config = null;
subscriptions = null;
categories = null;
sidenav = null;
locale = isoLangs['en'];
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
public snackBar: MatSnackBar, private titleService: Title) {
public snackBar: MatSnackBar) {
console.log('PostsService Initialized...');
this.path = this.document.location.origin + '/api/';
@@ -89,7 +85,6 @@ export class PostsService implements CanActivate {
const result = !this.debugMode ? res['config_file'] : res;
if (result) {
this.config = result['YoutubeDLMaterial'];
this.titleService.setTitle(this.config['Extra']['title_top']);
if (this.config['Advanced']['multi_user_mode']) {
this.checkAdminCreationStatus();
// login stuff
@@ -119,17 +114,6 @@ 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 => {
@@ -214,8 +198,12 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'setConfig', {new_config_file: config}, this.httpOptions);
}
deleteFile(uid: string, type: string, blacklistMode = false) {
return this.http.post(this.path + 'deleteFile', {uid: uid, type: type, blacklistMode: blacklistMode}, this.httpOptions);
deleteFile(uid: string, isAudio: boolean, blacklistMode = false) {
if (isAudio) {
return this.http.post(this.path + 'deleteMp3', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions);
} else {
return this.http.post(this.path + 'deleteMp4', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions);
}
}
getMp3s() {
@@ -234,16 +222,8 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions);
}
getFullTwitchChat(id, type, uuid = null) {
return this.http.post(this.path + 'getFullTwitchChat', {id: id, type: type, uuid: uuid}, this.httpOptions);
}
downloadTwitchChat(id, type, vodId, uuid = null) {
return this.http.post(this.path + 'downloadTwitchChatByVODID', {id: id, type: type, vodId: vodId, uuid: uuid}, this.httpOptions);
}
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
uid = null, uuid = null, id = null) {
uid = null, uuid = null) {
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
type: type,
zip_mode: Array.isArray(fileName),
@@ -252,8 +232,7 @@ export class PostsService implements CanActivate {
subscriptionName: subscriptionName,
subPlaylist: subPlaylist,
uuid: uuid,
uid: uid,
id: id
uid: uid
},
{responseType: 'blob', params: this.httpOptions.params});
}
@@ -317,39 +296,9 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions);
}
// categories
getAllCategories() {
return this.http.post(this.path + 'getAllCategories', {}, this.httpOptions);
}
createCategory(name) {
return this.http.post(this.path + 'createCategory', {name: name}, this.httpOptions);
}
deleteCategory(category_uid) {
return this.http.post(this.path + 'deleteCategory', {category_uid: category_uid}, this.httpOptions);
}
updateCategory(category) {
return this.http.post(this.path + 'updateCategory', {category: category}, this.httpOptions);
}
updateCategories(categories) {
return this.http.post(this.path + 'updateCategories', {categories: categories}, this.httpOptions);
}
reloadCategories() {
this.getAllCategories().subscribe(res => {
this.categories = res['categories'];
});
}
createSubscription(url, name, timerange = null, streamingOnly = false, maxQuality = 'best', audioOnly = false,
customArgs = null, customFileOutput = null) {
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, maxQuality: maxQuality,
streamingOnly: streamingOnly, audioOnly: audioOnly, customArgs: customArgs,
customFileOutput: customFileOutput}, this.httpOptions);
createSubscription(url, name, timerange = null, streamingOnly = false, audioOnly = false, customArgs = null, customFileOutput = null) {
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly,
audioOnly: audioOnly, customArgs: customArgs, customFileOutput: customFileOutput}, this.httpOptions);
}
updateSubscription(subscription) {
@@ -365,8 +314,8 @@ export class PostsService implements CanActivate {
file_uid: file_uid}, this.httpOptions)
}
getSubscription(id, name = null) {
return this.http.post(this.path + 'getSubscription', {id: id, name: name}, this.httpOptions);
getSubscription(id) {
return this.http.post(this.path + 'getSubscription', {id: id}, this.httpOptions);
}
getAllSubscriptions() {

View File

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

View File

@@ -116,47 +116,14 @@
</div>
<div class="col-12 mt-4">
<mat-form-field class="text-field" color="accent">
<input matInput [(ngModel)]="new_config['Downloader']['default_file_output']" matInput placeholder="Default file output" i18n-placeholder="Default file output placeholder">
<mat-hint><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Youtube-dl output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the above download paths. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-4 mb-5">
<mat-form-field class="text-field" style="margin-right: 12px;" color="accent">
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Global custom args" i18n-placeholder="Custom args input placeholder"></textarea>
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Custom args" i18n-placeholder="Custom args input placeholder"></textarea>
<mat-hint><ng-container i18n="Custom args setting input hint">Global custom args for downloads on the home page. Args are delimited using two commas like so: ,,</ng-container></mat-hint>
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
</mat-form-field>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3 mb-2">
<h6 i18n="Categories">Categories</h6>
<div cdkDropList class="category-list" (cdkDropListDropped)="dropCategory($event)">
<div class="category-box" *ngFor="let category of postsService.categories" cdkDrag>
<div class="category-custom-placeholder" *cdkDragPlaceholder></div>
{{category['name']}}
<span style="float: right">
<button mat-icon-button (click)="openEditCategoryDialog(category)"><mat-icon>edit</mat-icon></button>
<button mat-icon-button (click)="deleteCategory(category)"><mat-icon>cancel</mat-icon></button>
</span>
</div>
</div>
<button style="margin-top: 10px;" mat-mini-fab (click)="openAddCategoryDialog()"><mat-icon>add</mat-icon></button>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<div class="col-12 mt-5">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
</div>
@@ -228,24 +195,12 @@
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_youtube_API']"><ng-container i18n="Use YouTube API setting">Use YouTube API</ng-container></mat-checkbox>
</div>
<div class="col-12 mb-2">
<div class="col-12 mb-3">
<mat-form-field class="text-field" color="accent">
<input [disabled]="!new_config['API']['use_youtube_API']" [(ngModel)]="new_config['API']['youtube_API_key']" matInput placeholder="Youtube API Key" i18n-placeholder="Youtube API Key setting placeholder" required>
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_twitch_API']"><ng-container i18n="Use Twitch API setting">Use Twitch API</ng-container></mat-checkbox>
</div>
<div *ngIf="new_config['API']['use_twitch_API']" class="col-12 mt-1">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
</div>
<div class="col-12 mb-5">
<mat-form-field class="text-field" color="accent">
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_API_key']" matInput placeholder="Twitch API Key" i18n-placeholder="Twitch API Key setting placeholder" required>
<mat-hint><ng-container i18n="Twitch API Key setting hint AKA preamble">Also known as a Client ID.</ng-container>&nbsp;<a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
</div>
</div>
<mat-divider></mat-divider>
@@ -280,20 +235,11 @@
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-form-field>
<mat-label><ng-container i18n="Default downloader select label">Select a downloader</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['default_downloader']">
<mat-option value="youtube-dlc">youtube-dlc</mat-option>
<mat-option value="youtube-dl">youtube-dl</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mt-1">
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['use_default_downloading_agent']"><ng-container i18n="Use default downloading agent setting">Use default downloading agent</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field>
<mat-label><ng-container i18n="Custom downloader select label">Select a download agent</ng-container></mat-label>
<mat-label><ng-container i18n="Custom downloader select label">Select a downloader</ng-container></mat-label>
<mat-select [disabled]="new_config['Advanced']['use_default_downloading_agent']" color="accent" [(ngModel)]="new_config['Advanced']['custom_downloading_agent']">
<mat-option value="aria2c">aria2c</mat-option>
<mat-option value="avconv">avconv</mat-option>
@@ -305,9 +251,9 @@
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<div class="col-12 mt-2 mb-1">
<mat-form-field>
<mat-label><ng-container i18n="Log Level label">Log Level</ng-container></mat-label>
<mat-label><ng-container i18n="Logger level select label">Select a logger level</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['logger_level']">
<mat-option value="debug">Debug</mat-option>
<mat-option value="verbose">Verbose</mat-option>
@@ -317,7 +263,7 @@
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mb-1">
<div class="col-12 mt-2 mb-1">
<mat-form-field>
<mat-label><ng-container i18n="Login expiration select label">Login expiration</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['jwt_expiration']">

View File

@@ -30,55 +30,4 @@
margin-left: 15px;
margin-bottom: 12px;
bottom: 4px;
}
.category-list {
width: 500px;
max-width: 100%;
border: solid 1px #ccc;
min-height: 60px;
display: block;
// background: white;
border-radius: 4px;
overflow: hidden;
}
.category-box {
padding: 20px 10px;
border-bottom: solid 1px #ccc;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
cursor: move;
// background: white;
font-size: 14px;
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.category-box:last-child {
border: none;
}
.category-list.cdk-drop-list-dragging .category-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.category-custom-placeholder {
background: #ccc;
border: dotted 3px #999;
min-height: 60px;
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

View File

@@ -9,9 +9,6 @@ import { CURRENT_VERSION } from 'app/consts';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { CookiesUploaderDialogComponent } from 'app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/edit-category-dialog.component';
@Component({
selector: 'app-settings',
@@ -20,7 +17,7 @@ import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/ed
})
export class SettingsComponent implements OnInit {
all_locales = isoLangs;
supported_locales = ['en', 'es', 'de', 'fr', 'zh', 'nb', 'en-GB'];
supported_locales = ['en', 'es', 'de'];
initialLocale = localStorage.getItem('locale');
initial_config = null;
@@ -80,74 +77,6 @@ export class SettingsComponent implements OnInit {
})
}
dropCategory(event: CdkDragDrop<string[]>) {
moveItemInArray(this.postsService.categories, event.previousIndex, event.currentIndex);
this.postsService.updateCategories(this.postsService.categories).subscribe(res => {
}, err => {
this.postsService.openSnackBar('Failed to update categories!');
});
}
openAddCategoryDialog() {
const done = new EventEmitter<any>();
const dialogRef = this.dialog.open(InputDialogComponent, {
width: '300px',
data: {
inputTitle: 'Name the category',
inputPlaceholder: 'Name',
submitText: 'Add',
doneEmitter: done
}
});
done.subscribe(name => {
// Eventually do additional checks on name
if (name) {
this.postsService.createCategory(name).subscribe(res => {
if (res['success']) {
this.postsService.reloadCategories();
dialogRef.close();
const new_category = res['new_category'];
this.openEditCategoryDialog(new_category);
}
});
}
});
}
deleteCategory(category) {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
dialogTitle: 'Delete category',
dialogText: `Would you like to delete ${category['name']}?`,
submitText: 'Delete',
warnSubmitColor: true
}
});
dialogRef.afterClosed().subscribe(confirmed => {
if (confirmed) {
this.postsService.deleteCategory(category['uid']).subscribe(res => {
if (res['success']) {
this.postsService.openSnackBar(`Successfully deleted ${category['name']}!`);
this.postsService.reloadCategories();
}
}, err => {
this.postsService.openSnackBar(`Failed to delete ${category['name']}!`);
});
}
});
}
openEditCategoryDialog(category) {
this.dialog.open(EditCategoryDialogComponent, {
data: {
category: category
}
});
}
generateAPIKey() {
this.postsService.generateNewAPIKey().subscribe(res => {
if (res['new_api_key']) {
@@ -233,8 +162,7 @@ export class SettingsComponent implements OnInit {
dialogTitle: 'Kill downloads',
dialogText: 'Are you sure you want to kill all downloads? Any subscription and non-subscription downloads will end immediately, though this operation may take a minute or so to complete.',
submitText: 'Kill all downloads',
doneEmitter: done,
warnSubmitColor: true
doneEmitter: done
}
});
done.subscribe(confirmed => {

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}]);
subPlaylist: this.subscription.isPlaylist, uuid: this.postsService.user ? this.postsService.user.uid : null}]);
}
}
@@ -164,7 +164,7 @@ export class SubscriptionComponent implements OnInit {
editSubscription() {
this.dialog.open(EditSubscriptionDialogComponent, {
data: {
sub: this.postsService.getSubscriptionByID(this.subscription.id)
sub: this.subscription
}
});
}

View File

@@ -7,11 +7,9 @@
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"use_youtubedl_archive": true,
"custom_args": "",
"safe_download_override": false,
"include_thumbnail": false,
"include_metadata": true
"safe_download_override": false
},
"Extra": {
"title_top": "YoutubeDL-Material",
@@ -35,20 +33,12 @@
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_check_interval": "30",
"subscriptions_use_youtubedl_archive": true
},
"Users": {
"base_path": "users/",
"allow_registration": true,
"auth_method": "internal",
"ldap_config": {
"url": "ldap://localhost:389",
"bindDN": "cn=root",
"bindCredentials": "secret",
"searchBase": "ou=passport-ldapauth",
"searchFilter": "(uid={{username}})"
}
"allow_registration": true
},
"Advanced": {
"use_default_downloading_agent": true,
@@ -57,7 +47,7 @@
"allow_advanced_download": true,
"jwt_expiration": 86400,
"logger_level": "debug",
"use_cookies": false
"use_cookies": true
}
}
}

View File

@@ -1,5 +1,5 @@
{
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Wiedergabeliste erstellen",
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Playlist erstellen",
"cff1428d10d59d14e45edec3c735a27b5482db59": "Name",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiodateien",
"a52dae09be10ca3a65da918533ced3d3f4992238": "Videos",
@@ -35,7 +35,7 @@
"c32ef07f8803a223a83ed17024b38e8d82292407": "Passwort",
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
"9779715ac05308973d8f1c8658b29b986e92450f": "Ihre Audiodateien befinden sich hier",
"47546e45bbb476baaaad38244db444c427ddc502": "Wiedergabelisten",
"47546e45bbb476baaaad38244db444c427ddc502": "Playlisten",
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Keine Wiedergabelisten verfügbar. Erstellen Sie eine aus Ihren heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
"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": "Herunterlader",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader",
"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 quelloffener YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "ist ein Open-Source YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "beinhaltet viele tolle Funktionen! API, Docker und Lokalisierung werden unter anderem unterstützt. Informieren Sie sich über alle unterstützten Funktionen auf Github.",
"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": "Heruntergeladene",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Wiedergabeliste teilen",
"822fab38216f64e8166d368b59fe756ca39d301b": "Downloads",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Playlist 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 Wiedergabeliste oder einen Kanal",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "URL der Wiedergabeliste oder des Kanales",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Playlist oder einen Kanal",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "Playlist oder Kanal URL",
"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. Wiedergabeliste wird abgerufen.",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Wiedergabeliste abonniert.",
"2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Playlist wird abgerufen...",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Playlisten abonniert.",
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Suchen",
"2054791b822475aeaea95c0119113de3200f5e1c": "Länge:",
"94e01842dcee90531caa52e4147f70679bac87fe": "Löschen und erneut herunterladen",
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent löschen",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Aktualisierungsprogramm",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
"1372e61c5bd06100844bd43b98b016aabc468f62": "Wählen Sie eine Version:",
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrieren",
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sitzungs-ID:",
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(aktuell)",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Herunterladen-Ereignisse verfügbar!",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Downloads verfügbar.",
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
@@ -194,32 +194,5 @@
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Aktionen",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Benutzer hinzufügen",
"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"
}
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rolle bearbeiten"
}

File diff suppressed because it is too large Load Diff

View File

@@ -197,49 +197,19 @@
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registros",
"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",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modify playlist",
"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",
"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",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Estos se agregan después de los argumentos estándar.",
"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"
"98b6ec9ec138186d663e64770267b67334353d63": "Salida de archivo personalizada",
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Los registros aparecerán aquí",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Líneas:"
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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