mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
118 Commits
concurrent
...
forever-to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84187b9474 | ||
|
|
dbeeb32d48 | ||
|
|
46087f622e | ||
|
|
5dd48035fb | ||
|
|
db53a12635 | ||
|
|
8cd21bf433 | ||
|
|
c33acfb3de | ||
|
|
562eaa1b9b | ||
|
|
ec7f04552f | ||
|
|
75fc09ed99 | ||
|
|
8aa354ac24 | ||
|
|
58a0dc4afe | ||
|
|
0e37d83740 | ||
|
|
27faff054e | ||
|
|
a71d9f5c7e | ||
|
|
759637c1cf | ||
|
|
33f23c3ca9 | ||
|
|
176c99f813 | ||
|
|
f7e0b3e86b | ||
|
|
bd2443b1e9 | ||
|
|
f689609941 | ||
|
|
1e9eec1b55 | ||
|
|
677af3712b | ||
|
|
5fd9d93007 | ||
|
|
7ee34d21eb | ||
|
|
66c184a2e6 | ||
|
|
13f6f698b7 | ||
|
|
b08325c1e3 | ||
|
|
070d3fed57 | ||
|
|
775a1766d8 | ||
|
|
dbefb66021 | ||
|
|
3241d6aaaf | ||
|
|
d014c6facb | ||
|
|
b25ab70732 | ||
|
|
f9b8e78655 | ||
|
|
acad7cc057 | ||
|
|
c3d91e89a8 | ||
|
|
97c5102eb9 | ||
|
|
865185d277 | ||
|
|
a36794fd4f | ||
|
|
6639305771 | ||
|
|
cca76dd248 | ||
|
|
d899f88164 | ||
|
|
09b3c752d9 | ||
|
|
71bb91b6e6 | ||
|
|
f9b1414460 | ||
|
|
6eb1e2f898 | ||
|
|
30505d0e8b | ||
|
|
48ab1836ca | ||
|
|
20cedb6c29 | ||
|
|
9f5b6122fa | ||
|
|
5321624604 | ||
|
|
8828af4174 | ||
|
|
2bb4860a36 | ||
|
|
ce3d540633 | ||
|
|
f7b152fcf6 | ||
|
|
f892a4a305 | ||
|
|
fc55961822 | ||
|
|
ebfa49240c | ||
|
|
9e60d9fe3e | ||
|
|
ecef8842ae | ||
|
|
8cc653787f | ||
|
|
0360469c5a | ||
|
|
5a90be7703 | ||
|
|
ff403d18d1 | ||
|
|
11284cb1b3 | ||
|
|
8b1a1a56e3 | ||
|
|
32370280ab | ||
|
|
240d6569fa | ||
|
|
2927a4564d | ||
|
|
5c94036625 | ||
|
|
7be90ccd94 | ||
|
|
01b6e22f83 | ||
|
|
b1385f451b | ||
|
|
f40ac49082 | ||
|
|
2756cfae17 | ||
|
|
dac5919ffb | ||
|
|
34245bd339 | ||
|
|
8d6ec819e6 | ||
|
|
b03b4d173b | ||
|
|
c8f219d5b0 | ||
|
|
ec3ab17507 | ||
|
|
5124e3b333 | ||
|
|
d09b244bc2 | ||
|
|
c0a385ce78 | ||
|
|
258d5ff495 | ||
|
|
fb5c13db27 | ||
|
|
92413bd360 | ||
|
|
7174ef5f57 | ||
|
|
73b9cf7893 | ||
|
|
7ff906fd35 | ||
|
|
6e084bd94a | ||
|
|
21b97911e8 | ||
|
|
ccb4819a94 | ||
|
|
ce8f90ca1d | ||
|
|
8469ae10ad | ||
|
|
117255b0b7 | ||
|
|
f0e73c1708 | ||
|
|
aa1e36ae35 | ||
|
|
a1841e84ca | ||
|
|
05909877f4 | ||
|
|
90af895552 | ||
|
|
9f908aa3fc | ||
|
|
b56b371ece | ||
|
|
84e54cb4d5 | ||
|
|
42aaecc13a | ||
|
|
aac11b2105 | ||
|
|
bbf94ef982 | ||
|
|
2876cf55db | ||
|
|
375d3b4f38 | ||
|
|
160cffc737 | ||
|
|
7aad7b7d24 | ||
|
|
380475b33e | ||
|
|
384d365cf9 | ||
|
|
1125de43d7 | ||
|
|
d11f77a6c9 | ||
|
|
c660c28422 | ||
|
|
a1b32e2851 |
20
.eslintrc.json
Normal file
20
.eslintrc.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment**
|
||||
- YoutubeDL-Material version
|
||||
- Docker tag: <tag> (optional)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here. For example, a YouTube link.
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@@ -15,13 +15,33 @@ jobs:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '12'
|
||||
cache: 'npm'
|
||||
- name: install dependencies
|
||||
run: |
|
||||
npm install
|
||||
cd backend
|
||||
npm install
|
||||
sudo npm install -g @angular/cli
|
||||
- name: prepare localization
|
||||
run: |
|
||||
sudo npm install -g xliff-to-json
|
||||
xliff-to-json ./src/assets/i18n
|
||||
- name: Set hash
|
||||
id: vars
|
||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "version.json"
|
||||
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||
dir: 'backend/'
|
||||
- name: build
|
||||
run: ng build --prod
|
||||
- name: prepare artifact upload
|
||||
@@ -35,7 +55,7 @@ jobs:
|
||||
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
|
||||
New-Item -Path ./build/youtubedl-material -Name users -ItemType Directory
|
||||
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
|
||||
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
|
||||
- name: upload build artifact
|
||||
|
||||
17
.github/workflows/docker-release.yml
vendored
17
.github/workflows/docker-release.yml
vendored
@@ -13,6 +13,23 @@ jobs:
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: prepare localization
|
||||
run: |
|
||||
sudo npm install -g xliff-to-json
|
||||
xliff-to-json ./src/assets/i18n
|
||||
- name: Set hash
|
||||
id: vars
|
||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "version.json"
|
||||
json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||
dir: 'backend/'
|
||||
- name: setup platform emulator
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: setup multi-arch docker build
|
||||
|
||||
17
.github/workflows/docker.yml
vendored
17
.github/workflows/docker.yml
vendored
@@ -10,6 +10,23 @@ jobs:
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: prepare localization
|
||||
run: |
|
||||
sudo npm install -g xliff-to-json
|
||||
xliff-to-json ./src/assets/i18n
|
||||
- name: Set hash
|
||||
id: vars
|
||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "version.json"
|
||||
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||
dir: 'backend/'
|
||||
- name: setup platform emulator
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: setup multi-arch docker build
|
||||
|
||||
25
.vscode/tasks.json
vendored
Normal file
25
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"problemMatcher": [],
|
||||
"label": "Dev: start frontend",
|
||||
"detail": "ng serve"
|
||||
},
|
||||
{
|
||||
"label": "Dev: start backend",
|
||||
"type": "shell",
|
||||
"command": "set YTDL_MODE=debug && node app.js",
|
||||
"options": {
|
||||
"cwd": "./backend"
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.12 as frontend
|
||||
FROM alpine:latest as frontend
|
||||
|
||||
RUN apk add --no-cache \
|
||||
npm
|
||||
@@ -15,14 +15,13 @@ RUN ng build --prod
|
||||
|
||||
#--------------#
|
||||
|
||||
FROM alpine:3.12
|
||||
FROM alpine:latest
|
||||
|
||||
ENV UID=1000 \
|
||||
GID=1000 \
|
||||
USER=youtube
|
||||
|
||||
ENV NO_UPDATE_NOTIFIER=true
|
||||
ENV FOREVER_ROOT=/app/.forever
|
||||
|
||||
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
|
||||
|
||||
@@ -30,13 +29,14 @@ RUN apk add --no-cache \
|
||||
ffmpeg \
|
||||
npm \
|
||||
python2 \
|
||||
python3 \
|
||||
su-exec \
|
||||
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
|
||||
atomicparsley
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
|
||||
RUN npm install forever -g
|
||||
RUN npm install pm2 -g
|
||||
RUN npm install && chown -R $UID:$GID ./
|
||||
|
||||
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
||||
@@ -44,4 +44,4 @@ COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
|
||||
|
||||
EXPOSE 17442
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
CMD [ "forever", "app.js" ]
|
||||
CMD [ "pm2-runtime", "pm2.config.js" ]
|
||||
|
||||
@@ -77,6 +77,10 @@ Alternatively, you can port forward the port specified in the config (defaults t
|
||||
|
||||
## Docker
|
||||
|
||||
### Host-specific instructions
|
||||
|
||||
If you're on a Synology NAS, unRAID or any other possible special case you can check if there's known issues or instructions both in the issue tracker and in the [Wiki!](https://github.com/Tzahi12345/YoutubeDL-Material/wiki#environment-specific-guideshelp)
|
||||
|
||||
### Setup
|
||||
|
||||
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.
|
||||
@@ -124,7 +128,7 @@ Official translators:
|
||||
* German - UnlimitedCookies
|
||||
* Chinese - TyRoyal
|
||||
|
||||
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
|
||||
See also the list of [contributors](https://github.com/Tzahi12345/YoutubeDL-Material/graphs/contributors) who participated in this project.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
18
backend/.eslintrc.json
Normal file
18
backend/.eslintrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parser": "esprima",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [],
|
||||
"rules": {
|
||||
},
|
||||
"root": true
|
||||
}
|
||||
1229
backend/app.js
1229
backend/app.js
File diff suppressed because it is too large
Load Diff
@@ -12,14 +12,16 @@
|
||||
"custom_args": "",
|
||||
"safe_download_override": false,
|
||||
"include_thumbnail": true,
|
||||
"include_metadata": true
|
||||
"include_metadata": true,
|
||||
"max_concurrent_downloads": 5,
|
||||
"download_rate_limit": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"allow_autoplay": true,
|
||||
"enable_downloads_manager": true,
|
||||
"allow_playlist_categorization": true
|
||||
},
|
||||
@@ -30,7 +32,9 @@
|
||||
"youtube_API_key": "",
|
||||
"use_twitch_API": false,
|
||||
"twitch_API_key": "",
|
||||
"twitch_auto_download_chat": false
|
||||
"twitch_auto_download_chat": false,
|
||||
"use_sponsorblock_API": false,
|
||||
"generate_NFO_files": false
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
@@ -55,7 +59,7 @@
|
||||
}
|
||||
},
|
||||
"Database": {
|
||||
"use_local_db": false,
|
||||
"use_local_db": true,
|
||||
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
|
||||
},
|
||||
"Advanced": {
|
||||
@@ -65,8 +69,8 @@
|
||||
"multi_user_mode": false,
|
||||
"allow_advanced_download": false,
|
||||
"use_cookies": false,
|
||||
"jwt_expiration": 86400,
|
||||
"jwt_expiration": 86400,
|
||||
"logger_level": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
const path = require('path');
|
||||
const config_api = require('../config');
|
||||
const consts = require('../consts');
|
||||
const fs = require('fs-extra');
|
||||
const logger = require('../logger');
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { uuid } = require('uuidv4');
|
||||
const bcrypt = require('bcryptjs');
|
||||
@@ -12,15 +12,13 @@ var JwtStrategy = require('passport-jwt').Strategy,
|
||||
ExtractJwt = require('passport-jwt').ExtractJwt;
|
||||
|
||||
// other required vars
|
||||
let logger = null;
|
||||
let db_api = null;
|
||||
let SERVER_SECRET = null;
|
||||
let JWT_EXPIRATION = null;
|
||||
let opts = null;
|
||||
let saltRounds = null;
|
||||
|
||||
exports.initialize = function(db_api, input_logger) {
|
||||
setLogger(input_logger)
|
||||
exports.initialize = function(db_api) {
|
||||
setDB(db_api);
|
||||
|
||||
/*************************
|
||||
@@ -53,10 +51,6 @@ exports.initialize = function(db_api, input_logger) {
|
||||
}));
|
||||
}
|
||||
|
||||
function setLogger(input_logger) {
|
||||
logger = input_logger;
|
||||
}
|
||||
|
||||
function setDB(input_db_api) {
|
||||
db_api = input_db_api;
|
||||
}
|
||||
@@ -140,7 +134,7 @@ exports.registerUser = async function(req, res) {
|
||||
|
||||
exports.login = async (username, password) => {
|
||||
const user = await db_api.getRecord('users', {name: username});
|
||||
if (!user) { logger.error(`User ${username} not found`); false }
|
||||
if (!user) { logger.error(`User ${username} not found`); return false }
|
||||
if (user.auth_method && user.auth_method !== 'internal') { return false }
|
||||
return await bcrypt.compare(password, user.passhash) ? user : false;
|
||||
}
|
||||
@@ -291,17 +285,12 @@ exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false
|
||||
return file;
|
||||
}
|
||||
|
||||
exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}).assign({fileNames: new_filenames});
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.removePlaylist = async function(user_uid, playlistID) {
|
||||
await db_api.removeRecord('playlist', {playlistID: playlistID});
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.getUserPlaylists = async function(user_uid, user_files = null) {
|
||||
exports.getUserPlaylists = async function(user_uid) {
|
||||
return await db_api.getRecords('playlists', {user_uid: user_uid});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
const config_api = require('./config');
|
||||
const utils = require('./utils');
|
||||
const logger = require('./logger');
|
||||
|
||||
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 setDB(input_db_api) { db_api = input_db_api }
|
||||
|
||||
function initialize(input_db, input_users_db, input_logger, input_db_api) {
|
||||
setDB(input_db, input_users_db, input_db_api);
|
||||
setLogger(input_logger);
|
||||
function initialize(input_db_api) {
|
||||
setDB(input_db_api);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -72,7 +67,7 @@ async function getCategoriesAsPlaylists(files = null) {
|
||||
const categories_as_playlists = [];
|
||||
const available_categories = await getCategories();
|
||||
if (available_categories && files) {
|
||||
for (category of available_categories) {
|
||||
for (let category of available_categories) {
|
||||
const files_that_match = utils.addUIDsToCategory(category, files);
|
||||
if (files_that_match && files_that_match.length > 0) {
|
||||
category['thumbnailURL'] = files_that_match[0].thumbnailURL;
|
||||
@@ -125,21 +120,21 @@ function applyCategoryRules(file_json, rules, category_name) {
|
||||
return rules_apply;
|
||||
}
|
||||
|
||||
async function addTagToVideo(tag, video, user_uid) {
|
||||
// TODO: Implement
|
||||
}
|
||||
// async function addTagToVideo(tag, video, user_uid) {
|
||||
// // TODO: Implement
|
||||
// }
|
||||
|
||||
async function removeTagFromVideo(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();
|
||||
}
|
||||
}
|
||||
// // 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,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const logger = require('./logger');
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
||||
@@ -5,11 +7,7 @@ const debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
|
||||
|
||||
var logger = null;
|
||||
function setLogger(input_logger) { logger = input_logger; }
|
||||
|
||||
function initialize(input_logger) {
|
||||
setLogger(input_logger);
|
||||
function initialize() {
|
||||
ensureConfigFileExists();
|
||||
ensureConfigItemsExist();
|
||||
}
|
||||
@@ -97,13 +95,13 @@ function getConfigItem(key) {
|
||||
}
|
||||
let path = CONFIG_ITEMS[key]['path'];
|
||||
const val = Object.byString(config_json, path);
|
||||
if (val === undefined && Object.byString(DEFAULT_CONFIG, path)) {
|
||||
if (val === undefined && Object.byString(DEFAULT_CONFIG, path) !== undefined) {
|
||||
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
|
||||
setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
|
||||
return Object.byString(DEFAULT_CONFIG, path);
|
||||
}
|
||||
return Object.byString(config_json, path);
|
||||
};
|
||||
}
|
||||
|
||||
function setConfigItem(key, value) {
|
||||
let success = false;
|
||||
@@ -175,7 +173,7 @@ module.exports = {
|
||||
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
|
||||
}
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
const DEFAULT_CONFIG = {
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"url": "http://example.com",
|
||||
@@ -189,14 +187,16 @@ DEFAULT_CONFIG = {
|
||||
"custom_args": "",
|
||||
"safe_download_override": false,
|
||||
"include_thumbnail": true,
|
||||
"include_metadata": true
|
||||
"include_metadata": true,
|
||||
"max_concurrent_downloads": 5,
|
||||
"download_rate_limit": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"allow_autoplay": true,
|
||||
"enable_downloads_manager": true,
|
||||
"allow_playlist_categorization": true
|
||||
},
|
||||
@@ -207,7 +207,9 @@ DEFAULT_CONFIG = {
|
||||
"youtube_API_key": "",
|
||||
"use_twitch_API": false,
|
||||
"twitch_API_key": "",
|
||||
"twitch_auto_download_chat": false
|
||||
"twitch_auto_download_chat": false,
|
||||
"use_sponsorblock_API": false,
|
||||
"generate_NFO_files": false
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
@@ -216,7 +218,7 @@ DEFAULT_CONFIG = {
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_check_interval": "86400",
|
||||
"redownload_fresh_uploads": false
|
||||
},
|
||||
"Users": {
|
||||
@@ -232,7 +234,7 @@ DEFAULT_CONFIG = {
|
||||
}
|
||||
},
|
||||
"Database": {
|
||||
"use_local_db": false,
|
||||
"use_local_db": true,
|
||||
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
|
||||
},
|
||||
"Advanced": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
let CONFIG_ITEMS = {
|
||||
exports.CONFIG_ITEMS = {
|
||||
// Host
|
||||
'ytdl_url': {
|
||||
'key': 'ytdl_url',
|
||||
@@ -42,6 +42,14 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_include_metadata',
|
||||
'path': 'YoutubeDLMaterial.Downloader.include_metadata'
|
||||
},
|
||||
'ytdl_max_concurrent_downloads': {
|
||||
'key': 'ytdl_max_concurrent_downloads',
|
||||
'path': 'YoutubeDLMaterial.Downloader.max_concurrent_downloads'
|
||||
},
|
||||
'ytdl_download_rate_limit': {
|
||||
'key': 'ytdl_download_rate_limit',
|
||||
'path': 'YoutubeDLMaterial.Downloader.download_rate_limit'
|
||||
},
|
||||
|
||||
// Extra
|
||||
'ytdl_title_top': {
|
||||
@@ -60,9 +68,9 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_download_only_mode',
|
||||
'path': 'YoutubeDLMaterial.Extra.download_only_mode'
|
||||
},
|
||||
'ytdl_allow_multi_download_mode': {
|
||||
'key': 'ytdl_allow_multi_download_mode',
|
||||
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
|
||||
'ytdl_allow_autoplay': {
|
||||
'key': 'ytdl_allow_autoplay',
|
||||
'path': 'YoutubeDLMaterial.Extra.allow_autoplay'
|
||||
},
|
||||
'ytdl_enable_downloads_manager': {
|
||||
'key': 'ytdl_enable_downloads_manager',
|
||||
@@ -102,6 +110,15 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_twitch_auto_download_chat',
|
||||
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
||||
},
|
||||
'ytdl_use_sponsorblock_api': {
|
||||
'key': 'ytdl_use_sponsorblock_api',
|
||||
'path': 'YoutubeDLMaterial.API.use_sponsorblock_API'
|
||||
},
|
||||
'ytdl_generate_nfo_files': {
|
||||
'key': 'ytdl_generate_nfo_files',
|
||||
'path': 'YoutubeDLMaterial.API.generate_NFO_files'
|
||||
},
|
||||
|
||||
|
||||
// Themes
|
||||
'ytdl_default_theme': {
|
||||
@@ -126,10 +143,6 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_subscriptions_check_interval',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||
},
|
||||
'ytdl_subscriptions_check_interval': {
|
||||
'key': 'ytdl_subscriptions_check_interval',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||
},
|
||||
'ytdl_subscriptions_redownload_fresh_uploads': {
|
||||
'key': 'ytdl_subscriptions_redownload_fresh_uploads',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.redownload_fresh_uploads'
|
||||
@@ -198,7 +211,7 @@ let CONFIG_ITEMS = {
|
||||
}
|
||||
};
|
||||
|
||||
AVAILABLE_PERMISSIONS = [
|
||||
exports.AVAILABLE_PERMISSIONS = [
|
||||
'filemanager',
|
||||
'settings',
|
||||
'subscriptions',
|
||||
@@ -207,8 +220,6 @@ AVAILABLE_PERMISSIONS = [
|
||||
'downloads_manager'
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
||||
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
|
||||
CURRENT_VERSION: 'v4.2'
|
||||
}
|
||||
exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details'
|
||||
|
||||
exports.CURRENT_VERSION = 'v4.2';
|
||||
|
||||
295
backend/db.js
295
backend/db.js
@@ -1,24 +1,31 @@
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
var utils = require('./utils')
|
||||
const { uuid } = require('uuidv4');
|
||||
const config_api = require('./config');
|
||||
const { MongoClient } = require("mongodb");
|
||||
const { uuid } = require('uuidv4');
|
||||
|
||||
const config_api = require('./config');
|
||||
var utils = require('./utils')
|
||||
const logger = require('./logger');
|
||||
|
||||
const low = require('lowdb')
|
||||
const FileSync = require('lowdb/adapters/FileSync');
|
||||
const { BehaviorSubject } = require('rxjs');
|
||||
const local_adapter = new FileSync('./appdata/local_db.json');
|
||||
const local_db = low(local_adapter);
|
||||
|
||||
var logger = null;
|
||||
var db = null;
|
||||
var users_db = null;
|
||||
var database = null;
|
||||
let database = null;
|
||||
exports.database_initialized = false;
|
||||
exports.database_initialized_bs = new BehaviorSubject(false);
|
||||
|
||||
const tables = {
|
||||
files: {
|
||||
name: 'files',
|
||||
primary_key: 'uid'
|
||||
primary_key: 'uid',
|
||||
text_search: {
|
||||
title: 'text',
|
||||
uploader: 'text',
|
||||
uid: 'text'
|
||||
}
|
||||
},
|
||||
playlists: {
|
||||
name: 'playlists',
|
||||
@@ -43,6 +50,10 @@ const tables = {
|
||||
name: 'roles',
|
||||
primary_key: 'key'
|
||||
},
|
||||
download_queue: {
|
||||
name: 'download_queue',
|
||||
primary_key: 'uid'
|
||||
},
|
||||
test: {
|
||||
name: 'test'
|
||||
}
|
||||
@@ -54,7 +65,7 @@ const local_db_defaults = {}
|
||||
tables_list.forEach(table => {local_db_defaults[table] = []});
|
||||
local_db.defaults(local_db_defaults).write();
|
||||
|
||||
let using_local_db = config_api.getConfigItem('ytdl_use_local_db');
|
||||
let using_local_db = null;
|
||||
|
||||
function setDB(input_db, input_users_db) {
|
||||
db = input_db; users_db = input_users_db;
|
||||
@@ -62,36 +73,37 @@ function setDB(input_db, input_users_db) {
|
||||
exports.users_db = input_users_db
|
||||
}
|
||||
|
||||
function setLogger(input_logger) {
|
||||
logger = input_logger;
|
||||
}
|
||||
|
||||
exports.initialize = (input_db, input_users_db, input_logger) => {
|
||||
exports.initialize = (input_db, input_users_db) => {
|
||||
setDB(input_db, input_users_db);
|
||||
setLogger(input_logger);
|
||||
|
||||
// must be done here to prevent getConfigItem from being called before init
|
||||
using_local_db = config_api.getConfigItem('ytdl_use_local_db');
|
||||
}
|
||||
|
||||
exports.connectToDB = async (retries = 5, no_fallback = false) => {
|
||||
if (using_local_db) return;
|
||||
const success = await exports._connectToDB();
|
||||
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
|
||||
if (using_local_db && !custom_connection_string) return;
|
||||
const success = await exports._connectToDB(custom_connection_string);
|
||||
if (success) return true;
|
||||
|
||||
logger.warn(`MongoDB connection failed! Retrying ${retries} times...`);
|
||||
const retry_delay_ms = 2000;
|
||||
for (let i = 0; i < retries; i++) {
|
||||
const retry_succeeded = await exports._connectToDB();
|
||||
if (retry_succeeded) {
|
||||
logger.info(`Successfully connected to DB after ${i+1} attempt(s)`);
|
||||
return true;
|
||||
}
|
||||
if (retries) {
|
||||
logger.warn(`MongoDB connection failed! Retrying ${retries} times...`);
|
||||
const retry_delay_ms = 2000;
|
||||
for (let i = 0; i < retries; i++) {
|
||||
const retry_succeeded = await exports._connectToDB();
|
||||
if (retry_succeeded) {
|
||||
logger.info(`Successfully connected to DB after ${i+1} attempt(s)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (i !== retries - 1) {
|
||||
logger.warn(`Retry ${i+1} failed, waiting ${retry_delay_ms}ms before trying again.`);
|
||||
await utils.wait(retry_delay_ms);
|
||||
} else {
|
||||
logger.warn(`Retry ${i+1} failed.`);
|
||||
if (i !== retries - 1) {
|
||||
logger.warn(`Retry ${i+1} failed, waiting ${retry_delay_ms}ms before trying again.`);
|
||||
await utils.wait(retry_delay_ms);
|
||||
} else {
|
||||
logger.warn(`Retry ${i+1} failed.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (no_fallback) {
|
||||
logger.error('Failed to connect to MongoDB. Verify your connection string is valid.');
|
||||
return;
|
||||
@@ -102,8 +114,8 @@ exports.connectToDB = async (retries = 5, no_fallback = false) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
exports._connectToDB = async () => {
|
||||
const uri = config_api.getConfigItem('ytdl_mongodb_connection_string'); // "mongodb://127.0.0.1:27017/?compressors=zlib&gssapiServiceName=mongodb";
|
||||
exports._connectToDB = async (custom_connection_string = null) => {
|
||||
const uri = !custom_connection_string ? config_api.getConfigItem('ytdl_mongodb_connection_string') : custom_connection_string; // "mongodb://127.0.0.1:27017/?compressors=zlib&gssapiServiceName=mongodb";
|
||||
const client = new MongoClient(uri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
@@ -112,6 +124,10 @@ exports._connectToDB = async () => {
|
||||
try {
|
||||
await client.connect();
|
||||
database = client.db('ytdl_material');
|
||||
|
||||
// avoid doing anything else if it's just a test
|
||||
if (custom_connection_string) return true;
|
||||
|
||||
const existing_collections = (await database.listCollections({}, { nameOnly: true }).toArray()).map(collection => collection.name);
|
||||
|
||||
const missing_tables = tables_list.filter(table => !(existing_collections.includes(table)));
|
||||
@@ -121,8 +137,13 @@ exports._connectToDB = async () => {
|
||||
|
||||
tables_list.forEach(async table => {
|
||||
const primary_key = tables[table]['primary_key'];
|
||||
if (!primary_key) return;
|
||||
await database.collection(table).createIndex({[primary_key]: 1}, { unique: true });
|
||||
if (primary_key) {
|
||||
await database.collection(table).createIndex({[primary_key]: 1}, { unique: true });
|
||||
}
|
||||
const text_search = tables[table]['text_search'];
|
||||
if (text_search) {
|
||||
await database.collection(table).createIndex(text_search);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch(err) {
|
||||
@@ -134,51 +155,17 @@ exports._connectToDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
exports.registerFileDB = async (file_path, type, multiUserMode = null, sub = null, customPath = null, category = null, cropFileSettings = null, file_object = null) => {
|
||||
let db_path = null;
|
||||
const file_id = utils.removeFileExtension(file_path);
|
||||
if (!file_object) file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub);
|
||||
if (!file_object) {
|
||||
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// if category exists, only include essential info
|
||||
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
|
||||
|
||||
// modify duration
|
||||
if (cropFileSettings) {
|
||||
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
|
||||
}
|
||||
|
||||
if (multiUserMode) file_object['user_uid'] = multiUserMode.user;
|
||||
|
||||
const file_obj = await registerFileDBManual(file_object);
|
||||
|
||||
// remove metadata JSON if needed
|
||||
if (!config_api.getConfigItem('ytdl_include_metadata')) {
|
||||
utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path)
|
||||
}
|
||||
|
||||
return file_obj;
|
||||
}
|
||||
|
||||
exports.registerFileDB2 = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
|
||||
if (!file_object) file_object = generateFileObject2(file_path, type);
|
||||
exports.registerFileDB = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
|
||||
if (!file_object) file_object = generateFileObject(file_path, type);
|
||||
if (!file_object) {
|
||||
logger.error(`Could not find associated JSON file for ${type} file ${file_path}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
utils.fixVideoMetadataPerms2(file_path, type);
|
||||
utils.fixVideoMetadataPerms(file_path, type);
|
||||
|
||||
// add thumbnail path
|
||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail2(file_path, type);
|
||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_path);
|
||||
|
||||
// if category exists, only include essential info
|
||||
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
|
||||
@@ -195,7 +182,7 @@ exports.registerFileDB2 = async (file_path, type, user_uid = null, category = nu
|
||||
|
||||
// remove metadata JSON if needed
|
||||
if (!config_api.getConfigItem('ytdl_include_metadata')) {
|
||||
utils.deleteJSONFile2(file_path, type)
|
||||
utils.deleteJSONFile(file_path, type)
|
||||
}
|
||||
|
||||
return file_obj;
|
||||
@@ -213,39 +200,13 @@ async function registerFileDBManual(file_object) {
|
||||
return file_object;
|
||||
}
|
||||
|
||||
function generateFileObject(id, type, customPath = null, sub = null) {
|
||||
if (!customPath && sub) {
|
||||
customPath = getAppendedBasePathSub(sub, config_api.getConfigItem('ytdl_subscriptions_base_path'));
|
||||
}
|
||||
var jsonobj = (type === 'audio') ? utils.getJSONMp3(id, customPath, true) : utils.getJSONMp4(id, customPath, true);
|
||||
if (!jsonobj) {
|
||||
return null;
|
||||
}
|
||||
const ext = (type === 'audio') ? '.mp3' : '.mp4'
|
||||
const file_path = utils.getTrueFileName(jsonobj['_filename'], type); // path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext);
|
||||
// console.
|
||||
var stats = fs.statSync(path.join(__dirname, file_path));
|
||||
|
||||
var title = jsonobj.title;
|
||||
var url = jsonobj.webpage_url;
|
||||
var uploader = jsonobj.uploader;
|
||||
var upload_date = jsonobj.upload_date;
|
||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
|
||||
|
||||
var size = stats.size;
|
||||
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = type === 'audio';
|
||||
var description = jsonobj.description;
|
||||
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
||||
return file_obj;
|
||||
}
|
||||
|
||||
function generateFileObject2(file_path, type) {
|
||||
function generateFileObject(file_path, type) {
|
||||
var jsonobj = utils.getJSON(file_path, type);
|
||||
if (!jsonobj) {
|
||||
return null;
|
||||
} else if (!jsonobj['_filename']) {
|
||||
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
|
||||
return null;
|
||||
}
|
||||
const ext = (type === 'audio') ? '.mp3' : '.mp4'
|
||||
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
|
||||
@@ -256,8 +217,7 @@ function generateFileObject2(file_path, type) {
|
||||
var title = jsonobj.title;
|
||||
var url = jsonobj.webpage_url;
|
||||
var uploader = jsonobj.uploader;
|
||||
var upload_date = jsonobj.upload_date;
|
||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
|
||||
var upload_date = utils.formatDateString(jsonobj.upload_date);
|
||||
|
||||
var size = stats.size;
|
||||
|
||||
@@ -352,10 +312,11 @@ exports.importUnregisteredFiles = async () => {
|
||||
const file = files[j];
|
||||
|
||||
// check if file exists in db, if not add it
|
||||
const file_is_registered = !!(await exports.getRecord('files', {id: file.id, sub_id: dir_to_check.sub_id}))
|
||||
const files_with_same_url = await exports.getRecords('files', {url: file.url, sub_id: dir_to_check.sub_id});
|
||||
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
|
||||
if (!file_is_registered) {
|
||||
// add additional info
|
||||
await exports.registerFileDB2(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
|
||||
await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
|
||||
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
||||
}
|
||||
}
|
||||
@@ -363,24 +324,6 @@ exports.importUnregisteredFiles = async () => {
|
||||
|
||||
}
|
||||
|
||||
exports.preimportUnregisteredSubscriptionFile = async (sub, appendedBasePath) => {
|
||||
const preimported_file_paths = [];
|
||||
|
||||
const files = await utils.getDownloadedFilesByType(appendedBasePath, sub.type);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
// check if file exists in db, if not add it
|
||||
const file_is_registered = await exports.getRecord('files', {id: file.id, sub_id: sub.id});
|
||||
if (!file_is_registered) {
|
||||
// add additional info
|
||||
await exports.registerFileDB2(file['path'], sub.type, sub.user_uid, null, sub.id, null, file);
|
||||
preimported_file_paths.push(file['path']);
|
||||
logger.verbose(`Preemptively added subscription file to the database: ${file.id}`);
|
||||
}
|
||||
}
|
||||
return preimported_file_paths;
|
||||
}
|
||||
|
||||
exports.addMetadataPropertyToDB = async (property_key) => {
|
||||
try {
|
||||
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
|
||||
@@ -405,22 +348,26 @@ exports.addMetadataPropertyToDB = async (property_key) => {
|
||||
}
|
||||
}
|
||||
|
||||
exports.createPlaylist = async (playlist_name, uids, type, thumbnail_url, user_uid = null) => {
|
||||
exports.createPlaylist = async (playlist_name, uids, type, user_uid = null) => {
|
||||
const first_video = await exports.getVideo(uids[0]);
|
||||
const thumbnailToUse = first_video['thumbnailURL'];
|
||||
|
||||
let new_playlist = {
|
||||
name: playlist_name,
|
||||
uids: uids,
|
||||
id: uuid(),
|
||||
thumbnailURL: thumbnail_url,
|
||||
thumbnailURL: thumbnailToUse,
|
||||
type: type,
|
||||
registered: Date.now(),
|
||||
randomize_order: false
|
||||
};
|
||||
|
||||
const duration = await exports.calculatePlaylistDuration(new_playlist, user_uid);
|
||||
new_playlist.duration = duration;
|
||||
|
||||
new_playlist.user_uid = user_uid ? user_uid : undefined;
|
||||
|
||||
await exports.insertRecordIntoTable('playlists', new_playlist);
|
||||
|
||||
const duration = await exports.calculatePlaylistDuration(new_playlist);
|
||||
await exports.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
|
||||
|
||||
return new_playlist;
|
||||
}
|
||||
@@ -456,10 +403,10 @@ exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = fal
|
||||
return playlist;
|
||||
}
|
||||
|
||||
exports.updatePlaylist = async (playlist, user_uid = null) => {
|
||||
exports.updatePlaylist = async (playlist) => {
|
||||
let playlistID = playlist.id;
|
||||
|
||||
const duration = await exports.calculatePlaylistDuration(playlist, user_uid);
|
||||
const duration = await exports.calculatePlaylistDuration(playlist);
|
||||
playlist.duration = duration;
|
||||
|
||||
return await exports.updateRecord('playlists', {id: playlistID}, playlist);
|
||||
@@ -479,12 +426,12 @@ exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = nul
|
||||
return success;
|
||||
}
|
||||
|
||||
exports.calculatePlaylistDuration = async (playlist, uuid, playlist_file_objs = null) => {
|
||||
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
|
||||
if (!playlist_file_objs) {
|
||||
playlist_file_objs = [];
|
||||
for (let i = 0; i < playlist['uids'].length; i++) {
|
||||
const uid = playlist['uids'][i];
|
||||
const file_obj = await exports.getVideo(uid, uuid);
|
||||
const file_obj = await exports.getVideo(uid);
|
||||
if (file_obj) playlist_file_objs.push(file_obj);
|
||||
}
|
||||
}
|
||||
@@ -581,7 +528,7 @@ exports.getVideoUIDByID = async (file_id, uuid = null) => {
|
||||
return file_obj ? file_obj['uid'] : null;
|
||||
}
|
||||
|
||||
exports.getVideo = async (file_uid, uuid = null, sub_id = null) => {
|
||||
exports.getVideo = async (file_uid) => {
|
||||
return await exports.getRecord('files', {uid: file_uid});
|
||||
}
|
||||
|
||||
@@ -606,7 +553,22 @@ exports.insertRecordIntoTable = async (table, doc, replaceFilter = null) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (replaceFilter) await database.collection(table).deleteMany(replaceFilter);
|
||||
if (replaceFilter) {
|
||||
const output = await database.collection(table).bulkWrite([
|
||||
{
|
||||
deleteMany: {
|
||||
filter: replaceFilter
|
||||
}
|
||||
},
|
||||
{
|
||||
insertOne: {
|
||||
document: doc
|
||||
}
|
||||
}
|
||||
]);
|
||||
logger.debug(`Inserted doc into ${table} with filter: ${JSON.stringify(replaceFilter)}`);
|
||||
return !!(output['result']['ok']);
|
||||
}
|
||||
|
||||
const output = await database.collection(table).insertOne(doc);
|
||||
logger.debug(`Inserted doc into ${table}`);
|
||||
@@ -663,13 +625,28 @@ exports.getRecord = async (table, filter_obj) => {
|
||||
return await database.collection(table).findOne(filter_obj);
|
||||
}
|
||||
|
||||
exports.getRecords = async (table, filter_obj = null) => {
|
||||
exports.getRecords = async (table, filter_obj = null, return_count = false, sort = null, range = null) => {
|
||||
// local db override
|
||||
if (using_local_db) {
|
||||
return filter_obj ? applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').value() : local_db.get(table).value();
|
||||
let cursor = filter_obj ? applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').value() : local_db.get(table).value();
|
||||
if (sort) {
|
||||
cursor = cursor.sort((a, b) => (a[sort['by']] > b[sort['by']] ? sort['order'] : sort['order']*-1));
|
||||
}
|
||||
if (range) {
|
||||
cursor = cursor.slice(range[0], range[1]);
|
||||
}
|
||||
return !return_count ? cursor : cursor.length;
|
||||
}
|
||||
|
||||
return filter_obj ? await database.collection(table).find(filter_obj).toArray() : await database.collection(table).find().toArray();
|
||||
const cursor = filter_obj ? database.collection(table).find(filter_obj) : database.collection(table).find();
|
||||
if (sort) {
|
||||
cursor.sort({[sort['by']]: sort['order']});
|
||||
}
|
||||
if (range) {
|
||||
cursor.skip(range[0]).limit(range[1] - range[0]);
|
||||
}
|
||||
|
||||
return !return_count ? await cursor.toArray() : await cursor.count();
|
||||
}
|
||||
|
||||
// Update
|
||||
@@ -767,26 +744,26 @@ exports.removeRecord = async (table, filter_obj) => {
|
||||
return !!(output['result']['ok']);
|
||||
}
|
||||
|
||||
exports.removeAllRecords = async (table = null) => {
|
||||
exports.removeAllRecords = async (table = null, filter_obj = null) => {
|
||||
// local db override
|
||||
const tables_to_remove = table ? [table] : tables_list;
|
||||
logger.debug(`Removing all records from: ${tables_to_remove} with filter: ${JSON.stringify(filter_obj)}`)
|
||||
if (using_local_db) {
|
||||
logger.debug(`Removing all records from: ${tables_to_remove}`)
|
||||
for (let i = 0; i < tables_to_remove.length; i++) {
|
||||
const table_to_remove = tables_to_remove[i];
|
||||
local_db.assign({[table_to_remove]: []}).write();
|
||||
logger.debug(`Removed all records from ${table_to_remove}`);
|
||||
if (filter_obj) applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
|
||||
else local_db.assign({[table_to_remove]: []}).write();
|
||||
logger.debug(`Successfully removed records from ${table_to_remove}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let success = true;
|
||||
logger.debug(`Removing all records from: ${tables_to_remove}`)
|
||||
for (let i = 0; i < tables_to_remove.length; i++) {
|
||||
const table_to_remove = tables_to_remove[i];
|
||||
|
||||
const output = await database.collection(table_to_remove).deleteMany({});
|
||||
logger.debug(`Removed all records from ${table_to_remove}`);
|
||||
const output = await database.collection(table_to_remove).deleteMany(filter_obj ? filter_obj : {});
|
||||
logger.debug(`Successfully removed records from ${table_to_remove}`);
|
||||
success &= !!(output['result']['ok']);
|
||||
}
|
||||
return success;
|
||||
@@ -974,6 +951,8 @@ exports.transferDB = async (local_to_remote) => {
|
||||
|
||||
config_api.setConfigItem('ytdl_use_local_db', using_local_db);
|
||||
|
||||
logger.debug('Transfer finished!');
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -993,10 +972,28 @@ const applyFilterLocalDB = (db_path, filter_obj, operation) => {
|
||||
if (filter_prop_value === undefined || filter_prop_value === null) {
|
||||
filtered &= record[filter_prop] === undefined || record[filter_prop] === null
|
||||
} else {
|
||||
filtered &= record[filter_prop] === filter_prop_value;
|
||||
if (typeof filter_prop_value === 'object') {
|
||||
if (filter_prop_value['$regex']) {
|
||||
filtered &= (record[filter_prop].search(new RegExp(filter_prop_value['$regex'], filter_prop_value['$options'])) !== -1);
|
||||
}
|
||||
} else {
|
||||
filtered &= record[filter_prop] === filter_prop_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
});
|
||||
return return_val;
|
||||
}
|
||||
}
|
||||
|
||||
// archive helper functions
|
||||
|
||||
async function writeToBlacklist(type, line) {
|
||||
const archivePath = path.join(__dirname, 'appdata', 'archives');
|
||||
let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
|
||||
// adds newline to the beginning of the line
|
||||
line.replace('\n', '');
|
||||
line.replace('\r', '');
|
||||
line = '\n' + line;
|
||||
await fs.appendFile(blacklistPath, line);
|
||||
}
|
||||
|
||||
632
backend/downloader.js
Normal file
632
backend/downloader.js
Normal file
@@ -0,0 +1,632 @@
|
||||
const fs = require('fs-extra');
|
||||
const { uuid } = require('uuidv4');
|
||||
const path = require('path');
|
||||
const mergeFiles = require('merge-files');
|
||||
const NodeID3 = require('node-id3')
|
||||
const glob = require('glob')
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
const youtubedl = require('youtube-dl');
|
||||
|
||||
const logger = require('./logger');
|
||||
const config_api = require('./config');
|
||||
const twitch_api = require('./twitch');
|
||||
const { create } = require('xmlbuilder2');
|
||||
const categories_api = require('./categories');
|
||||
const utils = require('./utils');
|
||||
|
||||
let db_api = null;
|
||||
|
||||
const mutex = new Mutex();
|
||||
let should_check_downloads = true;
|
||||
|
||||
const archivePath = path.join(__dirname, 'appdata', 'archives');
|
||||
|
||||
function setDB(input_db_api) { db_api = input_db_api }
|
||||
|
||||
exports.initialize = (input_db_api) => {
|
||||
setDB(input_db_api);
|
||||
categories_api.initialize(db_api);
|
||||
if (db_api.database_initialized) {
|
||||
setupDownloads();
|
||||
} else {
|
||||
db_api.database_initialized_bs.subscribe(init => {
|
||||
if (init) setupDownloads();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null) => {
|
||||
return await mutex.runExclusive(async () => {
|
||||
const download = {
|
||||
url: url,
|
||||
type: type,
|
||||
title: '',
|
||||
user_uid: user_uid,
|
||||
sub_id: sub_id,
|
||||
sub_name: sub_name,
|
||||
options: options,
|
||||
uid: uuid(),
|
||||
step_index: 0,
|
||||
paused: false,
|
||||
running: false,
|
||||
finished_step: true,
|
||||
error: null,
|
||||
percent_complete: null,
|
||||
finished: false,
|
||||
timestamp_start: Date.now()
|
||||
};
|
||||
await db_api.insertRecordIntoTable('download_queue', download);
|
||||
|
||||
should_check_downloads = true;
|
||||
return download;
|
||||
});
|
||||
}
|
||||
|
||||
exports.pauseDownload = async (download_uid) => {
|
||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||
if (download['paused']) {
|
||||
logger.warn(`Download ${download_uid} is already paused!`);
|
||||
return false;
|
||||
} else if (download['finished']) {
|
||||
logger.info(`Download ${download_uid} could not be paused before completing.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return await db_api.updateRecord('download_queue', {uid: download_uid}, {paused: true, running: false});
|
||||
}
|
||||
|
||||
exports.resumeDownload = async (download_uid) => {
|
||||
return await mutex.runExclusive(async () => {
|
||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||
if (!download['paused']) {
|
||||
logger.warn(`Download ${download_uid} is not paused!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = db_api.updateRecord('download_queue', {uid: download_uid}, {paused: false});
|
||||
should_check_downloads = true;
|
||||
return success;
|
||||
})
|
||||
}
|
||||
|
||||
exports.restartDownload = async (download_uid) => {
|
||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||
await exports.clearDownload(download_uid);
|
||||
const success = !!(await exports.createDownload(download['url'], download['type'], download['options'], download['user_uid']));
|
||||
|
||||
should_check_downloads = true;
|
||||
return success;
|
||||
}
|
||||
|
||||
exports.cancelDownload = async (download_uid) => {
|
||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||
if (download['cancelled']) {
|
||||
logger.warn(`Download ${download_uid} is already cancelled!`);
|
||||
return false;
|
||||
} else if (download['finished']) {
|
||||
logger.info(`Download ${download_uid} could not be cancelled before completing.`);
|
||||
return false;
|
||||
}
|
||||
return await db_api.updateRecord('download_queue', {uid: download_uid}, {cancelled: true, running: false});
|
||||
}
|
||||
|
||||
exports.clearDownload = async (download_uid) => {
|
||||
return await db_api.removeRecord('download_queue', {uid: download_uid});
|
||||
}
|
||||
|
||||
async function handleDownloadError(download_uid, error_message) {
|
||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {error: error_message, finished: true, running: false});
|
||||
}
|
||||
|
||||
async function setupDownloads() {
|
||||
await fixDownloadState();
|
||||
setInterval(checkDownloads, 1000);
|
||||
}
|
||||
|
||||
async function fixDownloadState() {
|
||||
const downloads = await db_api.getRecords('download_queue');
|
||||
downloads.sort((download1, download2) => download1.timestamp_start - download2.timestamp_start);
|
||||
const running_downloads = downloads.filter(download => !download['finished'] && !download['error']);
|
||||
for (let i = 0; i < running_downloads.length; i++) {
|
||||
const running_download = running_downloads[i];
|
||||
const update_obj = {finished_step: true, paused: true, running: false};
|
||||
if (running_download['step_index'] > 0) {
|
||||
update_obj['step_index'] = running_download['step_index'] - 1;
|
||||
}
|
||||
await db_api.updateRecord('download_queue', {uid: running_download['uid']}, update_obj);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDownloads() {
|
||||
if (!should_check_downloads) return;
|
||||
|
||||
const downloads = await db_api.getRecords('download_queue');
|
||||
downloads.sort((download1, download2) => download1.timestamp_start - download2.timestamp_start);
|
||||
|
||||
await mutex.runExclusive(async () => {
|
||||
// avoid checking downloads unnecessarily, but double check that should_check_downloads is still true
|
||||
const running_downloads = downloads.filter(download => !download['paused'] && !download['finished']);
|
||||
if (running_downloads.length === 0) {
|
||||
should_check_downloads = false;
|
||||
logger.verbose('Disabling checking downloads as none are available.');
|
||||
}
|
||||
return;
|
||||
});
|
||||
|
||||
let running_downloads_count = downloads.filter(download => download['running']).length;
|
||||
const waiting_downloads = downloads.filter(download => !download['paused'] && download['finished_step'] && !download['finished']);
|
||||
for (let i = 0; i < waiting_downloads.length; i++) {
|
||||
const waiting_download = waiting_downloads[i];
|
||||
const max_concurrent_downloads = config_api.getConfigItem('ytdl_max_concurrent_downloads');
|
||||
if (max_concurrent_downloads < 0 || running_downloads_count >= max_concurrent_downloads) break;
|
||||
|
||||
if (waiting_download['finished_step'] && !waiting_download['finished']) {
|
||||
// move to next step
|
||||
running_downloads_count++;
|
||||
if (waiting_download['step_index'] === 0) {
|
||||
collectInfo(waiting_download['uid']);
|
||||
} else if (waiting_download['step_index'] === 1) {
|
||||
downloadQueuedFile(waiting_download['uid']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function collectInfo(download_uid) {
|
||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||
if (download['paused']) {
|
||||
return;
|
||||
}
|
||||
logger.verbose(`Collecting info for download ${download_uid}`);
|
||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 1, finished_step: false, running: true});
|
||||
|
||||
const url = download['url'];
|
||||
const type = download['type'];
|
||||
const options = download['options'];
|
||||
|
||||
if (download['user_uid'] && !options.customFileFolderPath) {
|
||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
const user_path = path.join(usersFileFolder, download['user_uid'], type);
|
||||
options.customFileFolderPath = user_path + path.sep;
|
||||
}
|
||||
|
||||
let args = await exports.generateArgs(url, type, options, download['user_uid']);
|
||||
|
||||
// get video info prior to download
|
||||
let info = await getVideoInfoByURL(url, args, download_uid);
|
||||
|
||||
if (!info) {
|
||||
// info failed, error presumably already recorded
|
||||
return;
|
||||
}
|
||||
|
||||
let category = null;
|
||||
|
||||
// check if it fits into a category. If so, then get info again using new args
|
||||
if (!Array.isArray(info) || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
||||
|
||||
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
||||
if (category && category['custom_output']) {
|
||||
options.customOutput = category['custom_output'];
|
||||
options.noRelativePath = true;
|
||||
args = await exports.generateArgs(url, type, options, download['user_uid']);
|
||||
info = await getVideoInfoByURL(url, args, download_uid);
|
||||
}
|
||||
|
||||
// setup info required to calculate download progress
|
||||
|
||||
const expected_file_size = utils.getExpectedFileSize(info);
|
||||
|
||||
const files_to_check_for_progress = [];
|
||||
|
||||
// store info in download for future use
|
||||
if (Array.isArray(info)) {
|
||||
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
|
||||
} else {
|
||||
files_to_check_for_progress.push(utils.removeFileExtension(info['_filename']));
|
||||
}
|
||||
|
||||
const playlist_title = Array.isArray(info) ? info[0]['playlist_title'] || info[0]['playlist'] : null;
|
||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
|
||||
finished_step: true,
|
||||
running: false,
|
||||
options: options,
|
||||
files_to_check_for_progress: files_to_check_for_progress,
|
||||
expected_file_size: expected_file_size,
|
||||
title: playlist_title ? playlist_title : info['title']
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadQueuedFile(download_uid) {
|
||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||
if (download['paused']) {
|
||||
return;
|
||||
}
|
||||
logger.verbose(`Downloading ${download_uid}`);
|
||||
return new Promise(async resolve => {
|
||||
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false, running: true});
|
||||
|
||||
const url = download['url'];
|
||||
const type = download['type'];
|
||||
const options = download['options'];
|
||||
const args = download['args'];
|
||||
const category = download['category'];
|
||||
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
|
||||
if (options.customFileFolderPath) {
|
||||
fileFolderPath = options.customFileFolderPath;
|
||||
}
|
||||
fs.ensureDirSync(fileFolderPath);
|
||||
|
||||
const start_time = Date.now();
|
||||
|
||||
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
|
||||
|
||||
// download file
|
||||
youtubedl.exec(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
||||
const file_objs = [];
|
||||
let end_time = Date.now();
|
||||
let difference = (end_time - start_time)/1000;
|
||||
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
||||
clearInterval(download_checker);
|
||||
if (err) {
|
||||
logger.error(err.stderr);
|
||||
await handleDownloadError(download_uid, err.stderr);
|
||||
resolve(false);
|
||||
return;
|
||||
} else if (output) {
|
||||
if (output.length === 0 || output[0].length === 0) {
|
||||
// ERROR!
|
||||
const error_message = `No output received for video download, check if it exists in your archive.`;
|
||||
await handleDownloadError(download_uid, error_message);
|
||||
logger.warn(error_message);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// get filepath with no extension
|
||||
const filepath_no_extension = utils.removeFileExtension(output_json['_filename']);
|
||||
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
var full_file_path = filepath_no_extension + ext;
|
||||
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
||||
|
||||
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||
let vodId = url.split('twitch.tv/videos/')[1];
|
||||
vodId = vodId.split('?')[0];
|
||||
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
||||
}
|
||||
|
||||
// renames file if necessary due to bug
|
||||
if (!fs.existsSync(output_json['_filename']) && fs.existsSync(output_json['_filename'] + '.webm')) {
|
||||
try {
|
||||
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
||||
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
|
||||
} catch(e) {
|
||||
logger.error(`Failed to rename file ${output_json['_filename']} to its appropriate extension.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'audio') {
|
||||
let tags = {
|
||||
title: output_json['title'],
|
||||
artist: output_json['artist'] ? output_json['artist'] : output_json['uploader']
|
||||
}
|
||||
let success = NodeID3.write(tags, utils.removeFileExtension(output_json['_filename']) + '.mp3');
|
||||
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
||||
}
|
||||
|
||||
if (config_api.getConfigItem('ytdl_generate_nfo_files')) {
|
||||
exports.generateNFOFile(output_json, `${filepath_no_extension}.nfo`);
|
||||
}
|
||||
|
||||
if (options.cropFileSettings) {
|
||||
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
||||
}
|
||||
|
||||
// registers file in DB
|
||||
const file_obj = await db_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
|
||||
|
||||
file_objs.push(file_obj);
|
||||
}
|
||||
|
||||
if (options.merged_string !== null && options.merged_string !== undefined) {
|
||||
const archive_folder = getArchiveFolder(fileFolderPath, options, download['user_uid']);
|
||||
const current_merged_archive = fs.readFileSync(path.join(archive_folder, `merged_${type}.txt`), 'utf8');
|
||||
const diff = current_merged_archive.replace(options.merged_string, '');
|
||||
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
|
||||
fs.appendFileSync(archive_path, diff);
|
||||
}
|
||||
|
||||
let container = null;
|
||||
|
||||
if (file_objs.length > 1) {
|
||||
// create playlist
|
||||
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
|
||||
container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), type, download['user_uid']);
|
||||
} else if (file_objs.length === 1) {
|
||||
container = file_objs[0];
|
||||
} else {
|
||||
const error_message = 'Downloaded file failed to result in metadata object.';
|
||||
logger.error(error_message);
|
||||
await handleDownloadError(download_uid, error_message);
|
||||
}
|
||||
|
||||
const file_uids = file_objs.map(file_obj => file_obj.uid);
|
||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {finished_step: true, finished: true, running: false, step_index: 3, percent_complete: 100, file_uids: file_uids, container: container});
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// helper functions
|
||||
|
||||
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
|
||||
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
|
||||
const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||
const globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
||||
const useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||
const is_audio = type === 'audio';
|
||||
|
||||
let fileFolderPath = is_audio ? audioFolderPath : videoFolderPath;
|
||||
|
||||
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
|
||||
|
||||
const customArgs = options.customArgs;
|
||||
let customOutput = options.customOutput;
|
||||
const customQualityConfiguration = options.customQualityConfiguration;
|
||||
|
||||
// video-specific args
|
||||
const selectedHeight = options.selectedHeight;
|
||||
|
||||
// audio-specific args
|
||||
const maxBitrate = options.maxBitrate;
|
||||
|
||||
const youtubeUsername = options.youtubeUsername;
|
||||
const youtubePassword = options.youtubePassword;
|
||||
|
||||
let downloadConfig = null;
|
||||
let qualityPath = (is_audio && !options.skip_audio_args) ? ['-f', 'bestaudio'] : ['-f', 'bestvideo+bestaudio', '--merge-output-format', 'mp4'];
|
||||
const is_youtube = url.includes('youtu');
|
||||
if (!is_audio && !is_youtube) {
|
||||
// tiktok videos fail when using the default format
|
||||
qualityPath = null;
|
||||
} else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) {
|
||||
qualityPath = ['-f', 'bestvideo+bestaudio']
|
||||
}
|
||||
|
||||
if (customArgs) {
|
||||
downloadConfig = customArgs.split(',,');
|
||||
} else {
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4'];
|
||||
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
|
||||
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
|
||||
} else if (is_audio) {
|
||||
qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0']
|
||||
}
|
||||
|
||||
if (customOutput) {
|
||||
customOutput = options.noRelativePath ? customOutput : path.join(fileFolderPath, customOutput);
|
||||
downloadConfig = ['-o', `${customOutput}.%(ext)s`, '--write-info-json', '--print-json'];
|
||||
} else {
|
||||
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json'];
|
||||
}
|
||||
|
||||
if (qualityPath) downloadConfig.push(...qualityPath);
|
||||
|
||||
if (is_audio && !options.skip_audio_args) {
|
||||
downloadConfig.push('-x');
|
||||
downloadConfig.push('--audio-format', 'mp3');
|
||||
}
|
||||
|
||||
if (youtubeUsername && youtubePassword) {
|
||||
downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword);
|
||||
}
|
||||
|
||||
if (useCookies) {
|
||||
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
||||
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
||||
} else {
|
||||
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
|
||||
}
|
||||
}
|
||||
|
||||
const useDefaultDownloadingAgent = config_api.getConfigItem('ytdl_use_default_downloading_agent');
|
||||
const customDownloadingAgent = config_api.getConfigItem('ytdl_custom_downloading_agent');
|
||||
if (!useDefaultDownloadingAgent && customDownloadingAgent) {
|
||||
downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent);
|
||||
}
|
||||
|
||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useYoutubeDLArchive) {
|
||||
const archive_folder = getArchiveFolder(fileFolderPath, options, user_uid);
|
||||
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
|
||||
|
||||
await fs.ensureDir(archive_folder);
|
||||
await fs.ensureFile(archive_path);
|
||||
|
||||
const blacklist_path = path.join(archive_folder, `blacklist_${type}.txt`);
|
||||
await fs.ensureFile(blacklist_path);
|
||||
|
||||
const merged_path = path.join(archive_folder, `merged_${type}.txt`);
|
||||
await fs.ensureFile(merged_path);
|
||||
// merges blacklist and regular archive
|
||||
let inputPathList = [archive_path, blacklist_path];
|
||||
await mergeFiles(inputPathList, merged_path);
|
||||
|
||||
options.merged_string = await fs.readFile(merged_path, "utf8");
|
||||
|
||||
downloadConfig.push('--download-archive', merged_path);
|
||||
}
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
downloadConfig.push('--write-thumbnail');
|
||||
}
|
||||
|
||||
if (globalArgs && globalArgs !== '') {
|
||||
// adds global args
|
||||
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
|
||||
// if global args has an output, replce the original output with that of global args
|
||||
const original_output_index = downloadConfig.indexOf('-o');
|
||||
downloadConfig.splice(original_output_index, 2);
|
||||
}
|
||||
downloadConfig = downloadConfig.concat(globalArgs.split(',,'));
|
||||
}
|
||||
|
||||
if (options.additionalArgs && options.additionalArgs !== '') {
|
||||
downloadConfig = downloadConfig.concat(options.additionalArgs.split(',,'));
|
||||
}
|
||||
|
||||
const rate_limit = config_api.getConfigItem('ytdl_download_rate_limit');
|
||||
if (rate_limit && downloadConfig.indexOf('-r') === -1 && downloadConfig.indexOf('--limit-rate') === -1) {
|
||||
downloadConfig.push('-r', rate_limit);
|
||||
}
|
||||
|
||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
||||
if (default_downloader === 'yt-dlp') {
|
||||
downloadConfig.push('--no-clean-infojson');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// filter out incompatible args
|
||||
downloadConfig = filterArgs(downloadConfig, is_audio);
|
||||
|
||||
if (!simulated) logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
|
||||
return downloadConfig;
|
||||
}
|
||||
|
||||
async function getVideoInfoByURL(url, args = [], download_uid = null) {
|
||||
return new Promise(resolve => {
|
||||
// remove bad args
|
||||
const new_args = [...args];
|
||||
|
||||
const archiveArgIndex = new_args.indexOf('--download-archive');
|
||||
if (archiveArgIndex !== -1) {
|
||||
new_args.splice(archiveArgIndex, 2);
|
||||
}
|
||||
|
||||
new_args.push('--dump-json');
|
||||
|
||||
youtubedl.exec(url, new_args, {maxBuffer: Infinity}, async (err, output) => {
|
||||
if (output) {
|
||||
let outputs = [];
|
||||
try {
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
outputs.push(output_json);
|
||||
}
|
||||
resolve(outputs.length === 1 ? outputs[0] : outputs);
|
||||
} catch(e) {
|
||||
const error = `Error while retrieving info on video with URL ${url} with the following message: output JSON could not be parsed. Output JSON: ${output}`;
|
||||
logger.error(error);
|
||||
if (download_uid) {
|
||||
await handleDownloadError(download_uid, error);
|
||||
}
|
||||
resolve(null);
|
||||
}
|
||||
} else {
|
||||
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
|
||||
if (err.stderr) error_message += `\n\n${err.stderr}`;
|
||||
logger.error(error_message);
|
||||
if (download_uid) {
|
||||
await handleDownloadError(download_uid, error_message);
|
||||
}
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function filterArgs(args, isAudio) {
|
||||
const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs'];
|
||||
const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail'];
|
||||
const args_to_remove = isAudio ? video_only_args : audio_only_args;
|
||||
return args.filter(x => !args_to_remove.includes(x));
|
||||
}
|
||||
|
||||
async function checkDownloadPercent(download_uid) {
|
||||
/*
|
||||
This is more of an art than a science, we're just selecting files that start with the file name,
|
||||
thus capturing the parts being downloaded in files named like so: '<video title>.<format>.<ext>.part'.
|
||||
|
||||
Any file that starts with <video title> will be counted as part of the "bytes downloaded", which will
|
||||
be divided by the "total expected bytes."
|
||||
*/
|
||||
|
||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||
const files_to_check_for_progress = download['files_to_check_for_progress'];
|
||||
const resulting_file_size = download['expected_file_size'];
|
||||
|
||||
if (!resulting_file_size) return;
|
||||
|
||||
let sum_size = 0;
|
||||
glob(`{${files_to_check_for_progress.join(',')}, }*`, async (err, files) => {
|
||||
files.forEach(async file => {
|
||||
try {
|
||||
const file_stats = fs.statSync(file);
|
||||
if (file_stats && file_stats.size) {
|
||||
sum_size += file_stats.size;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
});
|
||||
const percent_complete = (sum_size/resulting_file_size * 100).toFixed(2);
|
||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {percent_complete: percent_complete});
|
||||
});
|
||||
}
|
||||
|
||||
exports.generateNFOFile = (info, output_path) => {
|
||||
const nfo_obj = {
|
||||
episodedetails: {
|
||||
title: info['fulltitle'],
|
||||
episode: info['playlist_index'] ? info['playlist_index'] : undefined,
|
||||
premiered: utils.formatDateString(info['upload_date']),
|
||||
plot: `${info['uploader_url']}\n${info['description']}\n${info['playlist_title'] ? info['playlist_title'] : ''}`,
|
||||
director: info['artist'] ? info['artist'] : info['uploader']
|
||||
}
|
||||
};
|
||||
const doc = create(nfo_obj);
|
||||
const xml = doc.end({ prettyPrint: true });
|
||||
fs.writeFileSync(output_path, xml);
|
||||
}
|
||||
|
||||
function getArchiveFolder(fileFolderPath, options, user_uid) {
|
||||
if (options.customArchivePath) {
|
||||
return path.join(options.customArchivePath);
|
||||
} else if (user_uid) {
|
||||
return path.join(fileFolderPath, 'archives');
|
||||
} else {
|
||||
return path.join(archivePath);
|
||||
}
|
||||
}
|
||||
8
backend/ecosystem.config.js
Normal file
8
backend/ecosystem.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
apps : [{
|
||||
name : "YoutubeDL-Material",
|
||||
script : "./app.js",
|
||||
watch : "placeholder",
|
||||
watch_delay: 5000
|
||||
}]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
CMD="forever app.js"
|
||||
CMD="pm2-runtime pm2.config.js"
|
||||
|
||||
# if the first arg starts with "-" pass it to program
|
||||
if [ "${1#-}" != "$1" ]; then
|
||||
|
||||
23
backend/logger.js
Normal file
23
backend/logger.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const winston = require('winston');
|
||||
|
||||
let debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
const defaultFormat = winston.format.printf(({ level, message, label, timestamp }) => {
|
||||
return `${timestamp} ${level.toUpperCase()}: ${message}`;
|
||||
});
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.combine(winston.format.timestamp(), defaultFormat),
|
||||
defaultMeta: {},
|
||||
transports: [
|
||||
//
|
||||
// - Write to all logs with level `info` and below to `combined.log`
|
||||
// - Write all logs error (and below) to `error.log`.
|
||||
//
|
||||
new winston.transports.File({ filename: 'appdata/logs/error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'appdata/logs/combined.log' }),
|
||||
new winston.transports.Console({level: !debugMode ? 'info' : 'debug', name: 'console'})
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
258
backend/package-lock.json
generated
258
backend/package-lock.json
generated
@@ -4,6 +4,48 @@
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@dabh/diagnostics": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz",
|
||||
"integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==",
|
||||
"requires": {
|
||||
"colorspace": "1.1.x",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@oozcitak/dom": {
|
||||
"version": "1.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
|
||||
"integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
|
||||
"requires": {
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/url": "1.0.4",
|
||||
"@oozcitak/util": "8.3.8"
|
||||
}
|
||||
},
|
||||
"@oozcitak/infra": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
|
||||
"integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
|
||||
"requires": {
|
||||
"@oozcitak/util": "8.3.8"
|
||||
}
|
||||
},
|
||||
"@oozcitak/url": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
|
||||
"integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
|
||||
"requires": {
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/util": "8.3.8"
|
||||
}
|
||||
},
|
||||
"@oozcitak/util": {
|
||||
"version": "8.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
|
||||
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
|
||||
},
|
||||
"@sindresorhus/is": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
||||
@@ -287,6 +329,14 @@
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
|
||||
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
|
||||
},
|
||||
"async-mutex": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.1.tgz",
|
||||
"integrity": "sha512-vRfQwcqBnJTLzVQo72Sf7KIUbcSUP5hNchx6udI1U6LuPQpfePgdjJzlCe76yFZ8pxlLjn9lwcl/Ya0TSOv0Tw==",
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -308,11 +358,11 @@
|
||||
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||
"version": "0.21.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.2.tgz",
|
||||
"integrity": "sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"backoff": {
|
||||
@@ -677,19 +727,14 @@
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"color-string": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
|
||||
"integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz",
|
||||
"integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==",
|
||||
"requires": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"colornames": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz",
|
||||
"integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y="
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
|
||||
@@ -943,16 +988,6 @@
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||
},
|
||||
"diagnostics": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz",
|
||||
"integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==",
|
||||
"requires": {
|
||||
"colorspace": "1.1.x",
|
||||
"enabled": "1.0.x",
|
||||
"kuler": "1.0.x"
|
||||
}
|
||||
},
|
||||
"dicer": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
||||
@@ -1064,12 +1099,9 @@
|
||||
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
|
||||
},
|
||||
"enabled": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz",
|
||||
"integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=",
|
||||
"requires": {
|
||||
"env-variable": "0.0.x"
|
||||
}
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
@@ -1084,11 +1116,6 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"env-variable": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz",
|
||||
"integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg=="
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@@ -1109,6 +1136,11 @@
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||
},
|
||||
"esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||
},
|
||||
"etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
@@ -1193,15 +1225,10 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||
},
|
||||
"fast-safe-stringify": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
|
||||
"integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
|
||||
},
|
||||
"fecha": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
|
||||
"integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg=="
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz",
|
||||
"integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q=="
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
@@ -1258,10 +1285,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
|
||||
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
@@ -1776,12 +1808,9 @@
|
||||
}
|
||||
},
|
||||
"kuler": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz",
|
||||
"integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==",
|
||||
"requires": {
|
||||
"colornames": "^1.1.1"
|
||||
}
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
|
||||
},
|
||||
"latest-version": {
|
||||
"version": "5.1.0",
|
||||
@@ -1987,21 +2016,21 @@
|
||||
}
|
||||
},
|
||||
"logform": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz",
|
||||
"integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.3.0.tgz",
|
||||
"integrity": "sha512-graeoWUH2knKbGthMtuG1EfaSPMZFZBIrhuJHhkS5ZseFBrc7DupCzihOQAzsK/qIKPQaPJ/lFQFctILUY5ARQ==",
|
||||
"requires": {
|
||||
"colors": "^1.2.1",
|
||||
"fast-safe-stringify": "^2.0.4",
|
||||
"fecha": "^2.3.3",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^1.1.0",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2465,9 +2494,12 @@
|
||||
}
|
||||
},
|
||||
"one-time": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz",
|
||||
"integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4="
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||
"requires": {
|
||||
"fn.name": "1.x.x"
|
||||
}
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.0",
|
||||
@@ -2863,6 +2895,21 @@
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz",
|
||||
"integrity": "sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==",
|
||||
"requires": {
|
||||
"tslib": "~2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
|
||||
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
@@ -2874,6 +2921,11 @@
|
||||
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
|
||||
"optional": true
|
||||
},
|
||||
"safe-stable-stringify": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz",
|
||||
"integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -3007,6 +3059,11 @@
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
|
||||
@@ -3214,6 +3271,11 @@
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
|
||||
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
@@ -3459,42 +3521,27 @@
|
||||
}
|
||||
},
|
||||
"winston": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz",
|
||||
"integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz",
|
||||
"integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==",
|
||||
"requires": {
|
||||
"async": "^2.6.1",
|
||||
"diagnostics": "^1.1.1",
|
||||
"is-stream": "^1.1.0",
|
||||
"logform": "^2.1.1",
|
||||
"one-time": "0.0.4",
|
||||
"readable-stream": "^3.1.1",
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
"async": "^3.1.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.2.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
|
||||
}
|
||||
"winston-transport": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"winston-transport": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz",
|
||||
"integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz",
|
||||
"integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==",
|
||||
"requires": {
|
||||
"readable-stream": "^2.3.6",
|
||||
"readable-stream": "^2.3.7",
|
||||
"triple-beam": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -3565,6 +3612,37 @@
|
||||
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
|
||||
"integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q=="
|
||||
},
|
||||
"xmlbuilder2": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.0.2.tgz",
|
||||
"integrity": "sha512-h4MUawGY21CTdhV4xm3DG9dgsqyhDkZvVJBx88beqX8wJs3VgyGQgAn5VreHuae6unTQxh115aMK5InCVmOIKw==",
|
||||
"requires": {
|
||||
"@oozcitak/dom": "1.15.10",
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/util": "8.3.8",
|
||||
"@types/node": "*",
|
||||
"js-yaml": "3.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
|
||||
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"dependencies": {
|
||||
"archiver": "^3.1.1",
|
||||
"async": "^3.1.0",
|
||||
"axios": "^0.21.1",
|
||||
"async-mutex": "^0.3.1",
|
||||
"axios": "^0.21.2",
|
||||
"bcryptjs": "^2.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"config": "^3.2.3",
|
||||
@@ -60,10 +61,12 @@
|
||||
"progress": "^2.0.3",
|
||||
"ps-node": "^0.1.6",
|
||||
"read-last-lines": "^1.7.2",
|
||||
"rxjs": "^7.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"unzipper": "^0.10.10",
|
||||
"uuidv4": "^6.0.6",
|
||||
"winston": "^3.2.1",
|
||||
"winston": "^3.3.3",
|
||||
"xmlbuilder2": "^3.0.2",
|
||||
"youtube-dl": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
7
backend/pm2.config.js
Normal file
7
backend/pm2.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
apps : [{
|
||||
name : "YoutubeDL-Material",
|
||||
script : "./app.js",
|
||||
watch : "placeholder"
|
||||
}]
|
||||
}
|
||||
@@ -1,27 +1,21 @@
|
||||
const FileSync = require('lowdb/adapters/FileSync')
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const youtubedl = require('youtube-dl');
|
||||
|
||||
var fs = require('fs-extra');
|
||||
const { uuid } = require('uuidv4');
|
||||
var path = require('path');
|
||||
|
||||
var youtubedl = require('youtube-dl');
|
||||
const config_api = require('./config');
|
||||
const twitch_api = require('./twitch');
|
||||
var utils = require('./utils');
|
||||
const utils = require('./utils');
|
||||
const logger = require('./logger');
|
||||
|
||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
var logger = null;
|
||||
var db = null;
|
||||
var users_db = null;
|
||||
let db_api = null;
|
||||
let downloader_api = null;
|
||||
|
||||
function setDB(input_db_api) { db_api = input_db_api }
|
||||
function setLogger(input_logger) { logger = input_logger; }
|
||||
|
||||
function initialize(input_db_api, input_logger) {
|
||||
function initialize(input_db_api, input_downloader_api) {
|
||||
setDB(input_db_api);
|
||||
setLogger(input_logger);
|
||||
downloader_api = input_downloader_api;
|
||||
}
|
||||
|
||||
async function subscribe(sub, user_uid = null) {
|
||||
@@ -46,13 +40,13 @@ async function subscribe(sub, user_uid = null) {
|
||||
sub['user_uid'] = user_uid ? user_uid : undefined;
|
||||
await db_api.insertRecordIntoTable('subscriptions', sub);
|
||||
|
||||
let success = await getSubscriptionInfo(sub, user_uid);
|
||||
let success = await getSubscriptionInfo(sub);
|
||||
|
||||
if (success) {
|
||||
getVideosForSub(sub, user_uid);
|
||||
} else {
|
||||
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
||||
};
|
||||
}
|
||||
|
||||
result_obj.success = success;
|
||||
result_obj.sub = sub;
|
||||
@@ -61,13 +55,7 @@ async function subscribe(sub, user_uid = null) {
|
||||
|
||||
}
|
||||
|
||||
async function getSubscriptionInfo(sub, 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');
|
||||
|
||||
async function getSubscriptionInfo(sub) {
|
||||
// get videos
|
||||
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
|
||||
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||
@@ -114,22 +102,6 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
||||
}
|
||||
}
|
||||
|
||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useArchive && !sub.archive) {
|
||||
// must create the archive
|
||||
const archive_dir = path.join(__dirname, basePath, 'archives', sub.name);
|
||||
const archive_path = path.join(archive_dir, 'archive.txt');
|
||||
|
||||
// creates archive directory and text file if it doesn't exist
|
||||
fs.ensureDirSync(archive_dir);
|
||||
fs.ensureFileSync(archive_path);
|
||||
|
||||
// updates subscription
|
||||
sub.archive = archive_dir;
|
||||
|
||||
await db_api.updateRecord('subscriptions', {id: sub.id}, {archive: archive_dir});
|
||||
}
|
||||
|
||||
// TODO: get even more info
|
||||
|
||||
resolve(true);
|
||||
@@ -146,9 +118,23 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
|
||||
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;
|
||||
|
||||
const sub_files = await db_api.getRecords('files', {sub_id: id});
|
||||
for (let i = 0; i < sub_files.length; i++) {
|
||||
const sub_file = sub_files[i];
|
||||
if (config_api.descriptors[sub_file['uid']]) {
|
||||
try {
|
||||
for (let i = 0; i < config_api.descriptors[sub_file['uid']].length; i++) {
|
||||
config_api.descriptors[sub_file['uid']][i].destroy();
|
||||
}
|
||||
} catch(e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db_api.removeRecord('subscriptions', {id: id});
|
||||
await db_api.removeAllRecords('files', {sub_id: id});
|
||||
|
||||
@@ -249,30 +235,15 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
let appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
fs.ensureDirSync(appendedBasePath);
|
||||
|
||||
let multiUserMode = null;
|
||||
if (user_uid) {
|
||||
multiUserMode = {
|
||||
user: user_uid,
|
||||
file_path: appendedBasePath
|
||||
}
|
||||
}
|
||||
|
||||
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
|
||||
|
||||
// get videos
|
||||
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
||||
logger.verbose(`Subscription: getting videos for subscription ${sub.name} with args: ${downloadConfig.join(',')}`);
|
||||
|
||||
return new Promise(async resolve => {
|
||||
const preimported_file_paths = [];
|
||||
const PREIMPORT_INTERVAL = 5000;
|
||||
const preregister_check = setInterval(async () => {
|
||||
if (sub.streamingOnly) return;
|
||||
await db_api.preimportUnregisteredSubscriptionFile(sub, appendedBasePath);
|
||||
}, PREIMPORT_INTERVAL);
|
||||
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
|
||||
// cleanup
|
||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
||||
clearInterval(preregister_check);
|
||||
|
||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||
if (err && !output) {
|
||||
@@ -280,19 +251,21 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
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 {
|
||||
const outputs = err.stdout.split(/\r\n|\r|\n/);
|
||||
for (let i = 0; i < outputs.length; i++) {
|
||||
const output = JSON.parse(outputs[i]);
|
||||
await handleOutputJSON(sub, output, i === 0, multiUserMode)
|
||||
if (err.stderr.includes(output['id']) && archive_path) {
|
||||
// we found a video that errored! add it to the archive to prevent future errors
|
||||
if (sub.archive) {
|
||||
archive_dir = sub.archive;
|
||||
archive_path = path.join(archive_dir, 'archive.txt')
|
||||
fs.appendFileSync(archive_path, output['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: reimplement
|
||||
|
||||
// const outputs = err.stdout.split(/\r\n|\r|\n/);
|
||||
// for (let i = 0; i < outputs.length; i++) {
|
||||
// const output = JSON.parse(outputs[i]);
|
||||
// await handleOutputJSON(sub, output, i === 0, multiUserMode)
|
||||
// if (err.stderr.includes(output['id']) && archive_path) {
|
||||
// // we found a video that errored! add it to the archive to prevent future errors
|
||||
// if (sub.archive) {
|
||||
// archive_dir = sub.archive;
|
||||
// archive_path = path.join(archive_dir, 'archive.txt')
|
||||
// fs.appendFileSync(archive_path, output['id']);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
} catch(e) {
|
||||
logger.error('Backup method failed. See error below:');
|
||||
logger.error(e);
|
||||
@@ -305,21 +278,30 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
const output_jsons = [];
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
output_jsons.push(output_json);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const reset_videos = i === 0;
|
||||
await handleOutputJSON(sub, output_json, multiUserMode, preimported_file_paths, reset_videos);
|
||||
}
|
||||
|
||||
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
||||
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
|
||||
|
||||
for (let j = 0; j < files_to_download.length; j++) {
|
||||
const file_to_download = files_to_download[j];
|
||||
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name);
|
||||
}
|
||||
|
||||
resolve(files_to_download);
|
||||
|
||||
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
||||
await setFreshUploads(sub, user_uid);
|
||||
checkVideosForFreshUploads(sub, user_uid);
|
||||
@@ -331,10 +313,29 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
}, err => {
|
||||
logger.error(err);
|
||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
||||
clearInterval(preregister_check);
|
||||
});
|
||||
}
|
||||
|
||||
function generateOptionsForSubscriptionDownload(sub, user_uid) {
|
||||
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 default_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||
|
||||
const base_download_options = {
|
||||
selectedHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null,
|
||||
customFileFolderPath: getAppendedBasePath(sub, basePath),
|
||||
customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`,
|
||||
customArchivePath: path.join(__dirname, basePath, 'archives', sub.name),
|
||||
additionalArgs: sub.custom_args
|
||||
}
|
||||
|
||||
return base_download_options;
|
||||
}
|
||||
|
||||
async function generateArgsForSubscription(sub, user_uid, redownload = false, desired_path = null) {
|
||||
// get basePath
|
||||
let basePath = null;
|
||||
@@ -347,14 +348,16 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
|
||||
let appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
|
||||
let fullOutput = `${appendedBasePath}/%(title)s.%(ext)s`;
|
||||
const file_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||
|
||||
let fullOutput = `${appendedBasePath}/${file_output}.%(ext)s`;
|
||||
if (desired_path) {
|
||||
fullOutput = `${desired_path}.%(ext)s`;
|
||||
} else if (sub.custom_output) {
|
||||
fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`;
|
||||
}
|
||||
|
||||
let downloadConfig = ['-o', fullOutput, !redownload ? '-ciw' : '-ci', '--write-info-json', '--print-json'];
|
||||
let downloadConfig = ['--dump-json', '-o', fullOutput, !redownload ? '-ciw' : '-ci', '--write-info-json', '--print-json'];
|
||||
|
||||
let qualityPath = null;
|
||||
if (sub.type && sub.type === 'audio') {
|
||||
@@ -369,7 +372,7 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
downloadConfig.push(...qualityPath)
|
||||
|
||||
if (sub.custom_args) {
|
||||
customArgsArray = sub.custom_args.split(',,');
|
||||
const 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');
|
||||
@@ -411,46 +414,37 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
downloadConfig.push('--write-thumbnail');
|
||||
}
|
||||
|
||||
const rate_limit = config_api.getConfigItem('ytdl_download_rate_limit');
|
||||
if (rate_limit && downloadConfig.indexOf('-r') === -1 && downloadConfig.indexOf('--limit-rate') === -1) {
|
||||
downloadConfig.push('-r', rate_limit);
|
||||
}
|
||||
|
||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
||||
if (default_downloader === 'yt-dlp') {
|
||||
downloadConfig.push('--no-clean-infojson');
|
||||
}
|
||||
|
||||
return downloadConfig;
|
||||
}
|
||||
|
||||
async function handleOutputJSON(sub, output_json, multiUserMode = null, reset_videos = false) {
|
||||
// TODO: remove streaming only mode
|
||||
if (false && sub.streamingOnly) {
|
||||
if (reset_videos) {
|
||||
sub_db.assign({videos: []}).write();
|
||||
}
|
||||
|
||||
// remove unnecessary info
|
||||
output_json.formats = null;
|
||||
|
||||
// add to db
|
||||
sub_db.get('videos').push(output_json).write();
|
||||
} else {
|
||||
path_object = path.parse(output_json['_filename']);
|
||||
const path_string = path.format(path_object);
|
||||
|
||||
const file_exists = await db_api.getRecord('files', {path: path_string, sub_id: sub.id});
|
||||
if (file_exists) {
|
||||
// TODO: fix issue where files of different paths due to custom path get downloaded multiple times
|
||||
// file already exists in DB, return early to avoid reseting the download date
|
||||
return;
|
||||
}
|
||||
|
||||
await db_api.registerFileDB2(output_json['_filename'], sub.type, sub.user_uid, null, sub.id);
|
||||
|
||||
const url = output_json['webpage_url'];
|
||||
if (sub.type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||
const file_name = path.basename(output_json['_filename']);
|
||||
const id = file_name.substring(0, file_name.length-4);
|
||||
let vodId = url.split('twitch.tv/videos/')[1];
|
||||
vodId = vodId.split('?')[0];
|
||||
twitch_api.downloadTwitchChatByVODID(vodId, id, sub.type, multiUserMode.user, sub);
|
||||
async function getFilesToDownload(sub, output_jsons) {
|
||||
const files_to_download = [];
|
||||
for (let i = 0; i < output_jsons.length; i++) {
|
||||
const output_json = output_jsons[i];
|
||||
const file_missing = !(await db_api.getRecord('files', {sub_id: sub.id, url: output_json['webpage_url']})) && !(await db_api.getRecord('download_queue', {sub_id: sub.id, url: output_json['webpage_url'], error: null, finished: false}));
|
||||
if (file_missing) {
|
||||
const file_with_path_exists = await db_api.getRecord('files', {sub_id: sub.id, path: output_json['_filename']});
|
||||
if (file_with_path_exists) {
|
||||
// or maybe just overwrite???
|
||||
logger.info(`Skipping adding file ${output_json['_filename']} for subscription ${sub.name} as a file with that path already exists.`)
|
||||
}
|
||||
files_to_download.push(output_json);
|
||||
}
|
||||
}
|
||||
return files_to_download;
|
||||
}
|
||||
|
||||
|
||||
async function getSubscriptions(user_uid = null) {
|
||||
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
||||
}
|
||||
@@ -458,7 +452,7 @@ async function getSubscriptions(user_uid = null) {
|
||||
async function getAllSubscriptions() {
|
||||
const all_subs = await db_api.getRecords('subscriptions');
|
||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||
return all_subs.filter(sub => !!(sub.user_uid) === multiUserMode);
|
||||
return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode);
|
||||
}
|
||||
|
||||
async function getSubscription(subID) {
|
||||
@@ -469,7 +463,7 @@ async function getSubscriptionByName(subName, user_uid = null) {
|
||||
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
|
||||
}
|
||||
|
||||
async function updateSubscription(sub, user_uid = null) {
|
||||
async function updateSubscription(sub) {
|
||||
await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
|
||||
return true;
|
||||
}
|
||||
@@ -480,7 +474,7 @@ async function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
|
||||
});
|
||||
}
|
||||
|
||||
async function updateSubscriptionProperty(sub, assignment_obj, user_uid = null) {
|
||||
async function updateSubscriptionProperty(sub, assignment_obj) {
|
||||
// TODO: combine with updateSubscription
|
||||
await db_api.updateRecord('subscriptions', {id: sub.id}, assignment_obj);
|
||||
return true;
|
||||
@@ -535,7 +529,6 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
||||
// helper functions
|
||||
|
||||
function getAppendedBasePath(sub, base_path) {
|
||||
|
||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||
}
|
||||
|
||||
@@ -549,7 +542,7 @@ module.exports = {
|
||||
unsubscribe : unsubscribe,
|
||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
||||
getVideosForSub : getVideosForSub,
|
||||
setLogger : setLogger,
|
||||
initialize : initialize,
|
||||
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple
|
||||
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
|
||||
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
|
||||
}
|
||||
|
||||
1
backend/test/sample.info.json
Normal file
1
backend/test/sample.info.json
Normal file
File diff suppressed because one or more lines are too long
@@ -40,7 +40,7 @@ const subscriptions_api = require('../subscriptions');
|
||||
const fs = require('fs-extra');
|
||||
const { uuid } = require('uuidv4');
|
||||
|
||||
db_api.initialize(db, users_db, logger);
|
||||
db_api.initialize(db, users_db);
|
||||
|
||||
|
||||
describe('Database', async function() {
|
||||
@@ -76,24 +76,6 @@ describe('Database', async function() {
|
||||
|
||||
});
|
||||
|
||||
describe('Import and Export', async function() {
|
||||
it('Existing data', async function() {
|
||||
const users_db_json = users_db.value();
|
||||
const db_json = db.value();
|
||||
|
||||
const users_db_json_stringified = JSON.stringify(users_db_json);
|
||||
const db_json_stringified = JSON.stringify(db_json);
|
||||
|
||||
const tables_obj = await db_api.importJSONtoDB(users_db_json, db_json);
|
||||
const db_jsons = await db_api.exportDBToJSON(tables_obj);
|
||||
|
||||
const users_db_json_returned_stringified = db_jsons['users_db_json'];
|
||||
const db_json_returned_stringified = db_jsons['db_json'];
|
||||
|
||||
assert(users_db_json_returned_stringified.length === users_db_json_stringified.length);
|
||||
assert(db_json_returned_stringified.length === db_json_stringified.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Basic functions', async function() {
|
||||
beforeEach(async function() {
|
||||
@@ -186,6 +168,35 @@ describe('Database', async function() {
|
||||
const stats = await db_api.getDBStats();
|
||||
assert(stats);
|
||||
});
|
||||
|
||||
it('Query speed', async function() {
|
||||
this.timeout(120000);
|
||||
const NUM_RECORDS_TO_ADD = 300004; // max batch ops is 1000
|
||||
const test_records = [];
|
||||
let random_uid = '06241f83-d1b8-4465-812c-618dfa7f2943';
|
||||
for (let i = 0; i < NUM_RECORDS_TO_ADD; i++) {
|
||||
const uid = uuid();
|
||||
if (i === NUM_RECORDS_TO_ADD/2) random_uid = uid;
|
||||
test_records.push({"id":"A$AP Mob - Yamborghini High (Official Music Video) ft. Juicy J","title":"A$AP Mob - Yamborghini High (Official Music Video) ft. Juicy J","thumbnailURL":"https://i.ytimg.com/vi/tt7gP_IW-1w/maxresdefault.jpg","isAudio":true,"duration":312,"url":"https://www.youtube.com/watch?v=tt7gP_IW-1w","uploader":"asapmobVEVO","size":5060157,"path":"audio\\A$AP Mob - Yamborghini High (Official Music Video) ft. Juicy J.mp3","upload_date":"2016-05-11","description":"A$AP Mob ft. Juicy J - \"Yamborghini High\" Get it now on:\niTunes: http://smarturl.it/iYAMH?IQid=yt\nListen on Spotify: http://smarturl.it/sYAMH?IQid=yt\nGoogle Play: http://smarturl.it/gYAMH?IQid=yt\nAmazon: http://smarturl.it/aYAMH?IQid=yt\n\nFollow A$AP Mob:\nhttps://www.facebook.com/asapmobofficial\nhttps://twitter.com/ASAPMOB\nhttp://instagram.com/asapmob \nhttp://www.asapmob.com/\n\n#AsapMob #YamborghiniHigh #Vevo #HipHop #OfficialMusicVideo #JuicyJ","view_count":118689353,"height":null,"abr":160,"uid": uid,"registered":1626672120632});
|
||||
}
|
||||
const insert_start = Date.now();
|
||||
let success = await db_api.bulkInsertRecordsIntoTable('test', test_records);
|
||||
const insert_end = Date.now();
|
||||
|
||||
console.log(`Insert time: ${(insert_end - insert_start)/1000}s`);
|
||||
|
||||
const query_start = Date.now();
|
||||
const random_record = await db_api.getRecord('test', {uid: random_uid});
|
||||
const query_end = Date.now();
|
||||
|
||||
console.log(random_record)
|
||||
|
||||
console.log(`Query time: ${(query_end - query_start)/1000}s`);
|
||||
|
||||
success = !!random_record;
|
||||
|
||||
assert(success);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -275,5 +286,67 @@ describe('Multi User', async function() {
|
||||
// assert(video_obj);
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
describe('Downloader', function() {
|
||||
const downloader_api = require('../downloader');
|
||||
downloader_api.initialize(db_api);
|
||||
const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
|
||||
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
||||
const options = {
|
||||
ui_uid: uuid(),
|
||||
user: 'admin'
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await db_api.connectToDB();
|
||||
await db_api.removeAllRecords('download_queue');
|
||||
});
|
||||
|
||||
it('Get file info', async function() {
|
||||
|
||||
});
|
||||
|
||||
it('Download file', async function() {
|
||||
this.timeout(300000);
|
||||
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
||||
console.log(returned_download);
|
||||
await utils.wait(20000);
|
||||
|
||||
});
|
||||
|
||||
it('Queue file', async function() {
|
||||
this.timeout(300000);
|
||||
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
||||
console.log(returned_download);
|
||||
await utils.wait(20000);
|
||||
});
|
||||
|
||||
it('Pause file', async function() {
|
||||
|
||||
});
|
||||
|
||||
it('Generate args', async function() {
|
||||
const args = await downloader_api.generateArgs(url, 'video', options);
|
||||
console.log(args);
|
||||
});
|
||||
|
||||
it('Generate args - subscription', async function() {
|
||||
subscriptions_api.initialize(db_api, logger);
|
||||
const sub = await subscriptions_api.getSubscription(sub_id);
|
||||
const sub_options = subscriptions_api.generateOptionsForSubscriptionDownload(sub, 'admin');
|
||||
const args = await downloader_api.generateArgs(url, 'video', sub_options, 'admin');
|
||||
console.log(args);
|
||||
});
|
||||
|
||||
it('Generate kodi NFO file', async function() {
|
||||
const nfo_file_path = './test/sample.nfo';
|
||||
if (fs.existsSync(nfo_file_path)) {
|
||||
fs.unlinkSync(nfo_file_path);
|
||||
}
|
||||
const sample_json = fs.readJSONSync('./test/sample.info.json');
|
||||
downloader_api.generateNFOFile(sample_json, nfo_file_path);
|
||||
assert(fs.existsSync(nfo_file_path), true);
|
||||
});
|
||||
});
|
||||
|
||||
133
backend/utils.js
133
backend/utils.js
@@ -1,6 +1,9 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const config_api = require('./config');
|
||||
const logger = require('./logger');
|
||||
const CONSTS = require('./consts')
|
||||
const archiver = require('archiver');
|
||||
|
||||
const is_windows = process.platform === 'win32';
|
||||
@@ -42,8 +45,7 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
|
||||
files.push(jsonobj);
|
||||
continue;
|
||||
}
|
||||
var upload_date = jsonobj.upload_date;
|
||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : null;
|
||||
var upload_date = formatDateString(jsonobj.upload_date);
|
||||
|
||||
var isaudio = type === 'audio';
|
||||
var file_obj = new File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
|
||||
@@ -141,24 +143,7 @@ function getJSONByType(type, name, customPath, openReadPerms = false) {
|
||||
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
|
||||
}
|
||||
|
||||
function getDownloadedThumbnail(name, type, customPath = null) {
|
||||
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path') : config_api.getConfigItem('ytdl_video_folder_path');
|
||||
|
||||
let jpgPath = path.join(customPath, name + '.jpg');
|
||||
let webpPath = path.join(customPath, name + '.webp');
|
||||
let pngPath = path.join(customPath, name + '.png');
|
||||
|
||||
if (fs.existsSync(jpgPath))
|
||||
return jpgPath;
|
||||
else if (fs.existsSync(webpPath))
|
||||
return webpPath;
|
||||
else if (fs.existsSync(pngPath))
|
||||
return pngPath;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDownloadedThumbnail2(file_path, type) {
|
||||
function getDownloadedThumbnail(file_path) {
|
||||
const file_path_no_extension = removeFileExtension(file_path);
|
||||
|
||||
let jpgPath = file_path_no_extension + '.jpg';
|
||||
@@ -181,10 +166,6 @@ function getExpectedFileSize(input_info_jsons) {
|
||||
|
||||
let expected_filesize = 0;
|
||||
info_jsons.forEach(info_json => {
|
||||
if (info_json['filesize']) {
|
||||
expected_filesize += info_json['filesize'];
|
||||
return;
|
||||
}
|
||||
const formats = info_json['format_id'].split('+');
|
||||
let individual_expected_filesize = 0;
|
||||
formats.forEach(format_id => {
|
||||
@@ -200,29 +181,7 @@ function getExpectedFileSize(input_info_jsons) {
|
||||
return expected_filesize;
|
||||
}
|
||||
|
||||
function fixVideoMetadataPerms(name, type, customPath = null) {
|
||||
if (is_windows) return;
|
||||
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
|
||||
: config_api.getConfigItem('ytdl_video_folder_path');
|
||||
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
|
||||
const files_to_fix = [
|
||||
// JSONs
|
||||
path.join(customPath, name + '.info.json'),
|
||||
path.join(customPath, name + ext + '.info.json'),
|
||||
// Thumbnails
|
||||
path.join(customPath, name + '.webp'),
|
||||
path.join(customPath, name + '.jpg')
|
||||
];
|
||||
|
||||
for (const file of files_to_fix) {
|
||||
if (!fs.existsSync(file)) continue;
|
||||
fs.chmodSync(file, 0o644);
|
||||
}
|
||||
}
|
||||
|
||||
function fixVideoMetadataPerms2(file_path, type) {
|
||||
function fixVideoMetadataPerms(file_path, type) {
|
||||
if (is_windows) return;
|
||||
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
@@ -244,19 +203,7 @@ function fixVideoMetadataPerms2(file_path, type) {
|
||||
}
|
||||
}
|
||||
|
||||
function deleteJSONFile(name, type, customPath = null) {
|
||||
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
|
||||
: config_api.getConfigItem('ytdl_video_folder_path');
|
||||
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
let json_path = path.join(customPath, name + '.info.json');
|
||||
let alternate_json_path = path.join(customPath, name + ext + '.info.json');
|
||||
|
||||
if (fs.existsSync(json_path)) fs.unlinkSync(json_path);
|
||||
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
|
||||
}
|
||||
|
||||
function deleteJSONFile2(file_path, type) {
|
||||
function deleteJSONFile(file_path, type) {
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
|
||||
const file_path_no_extension = removeFileExtension(file_path);
|
||||
@@ -292,7 +239,6 @@ async function removeIDFromArchive(archive_path, id) {
|
||||
const updatedData = dataArray.join('\n');
|
||||
await fs.writeFile(archive_path, updatedData);
|
||||
if (line) return line;
|
||||
if (err) throw err;
|
||||
}
|
||||
|
||||
function durationStringToNumber(dur_str) {
|
||||
@@ -315,6 +261,11 @@ function addUIDsToCategory(category, files) {
|
||||
return files_that_match;
|
||||
}
|
||||
|
||||
function getCurrentDownloader() {
|
||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
||||
return details_json['downloader'];
|
||||
}
|
||||
|
||||
async function recFindByExt(base,ext,files,result)
|
||||
{
|
||||
files = files || (await fs.readdir(base))
|
||||
@@ -343,6 +294,57 @@ function removeFileExtension(filename) {
|
||||
return filename_parts.join('.');
|
||||
}
|
||||
|
||||
function formatDateString(date_string) {
|
||||
return date_string ? `${date_string.substring(0, 4)}-${date_string.substring(4, 6)}-${date_string.substring(6, 8)}` : 'N/A';
|
||||
}
|
||||
|
||||
function createEdgeNGrams(str) {
|
||||
if (str && str.length > 3) {
|
||||
const minGram = 3
|
||||
const maxGram = str.length
|
||||
|
||||
return str.split(" ").reduce((ngrams, token) => {
|
||||
if (token.length > minGram) {
|
||||
for (let i = minGram; i <= maxGram && i <= token.length; ++i) {
|
||||
ngrams = [...ngrams, token.substr(0, i)]
|
||||
}
|
||||
} else {
|
||||
ngrams = [...ngrams, token]
|
||||
}
|
||||
return ngrams
|
||||
}, []).join(" ")
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// ffmpeg helper functions
|
||||
|
||||
async function cropFile(file_path, start, end, ext) {
|
||||
return new Promise(resolve => {
|
||||
const temp_file_path = `${file_path}.cropped${ext}`;
|
||||
let base_ffmpeg_call = ffmpeg(file_path);
|
||||
if (start) {
|
||||
base_ffmpeg_call = base_ffmpeg_call.seekOutput(start);
|
||||
}
|
||||
if (end) {
|
||||
base_ffmpeg_call = base_ffmpeg_call.duration(end - start);
|
||||
}
|
||||
base_ffmpeg_call
|
||||
.on('end', () => {
|
||||
logger.verbose(`Cropping for '${file_path}' complete.`);
|
||||
fs.unlinkSync(file_path);
|
||||
fs.moveSync(temp_file_path, file_path);
|
||||
resolve(true);
|
||||
})
|
||||
.on('error', (err) => {
|
||||
logger.error(`Failed to crop ${file_path}.`);
|
||||
logger.error(err);
|
||||
resolve(false);
|
||||
}).save(temp_file_path);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* setTimeout, but its a promise.
|
||||
* @param {number} ms
|
||||
@@ -378,20 +380,21 @@ module.exports = {
|
||||
getJSON: getJSON,
|
||||
getTrueFileName: getTrueFileName,
|
||||
getDownloadedThumbnail: getDownloadedThumbnail,
|
||||
getDownloadedThumbnail2: getDownloadedThumbnail2,
|
||||
getExpectedFileSize: getExpectedFileSize,
|
||||
fixVideoMetadataPerms: fixVideoMetadataPerms,
|
||||
fixVideoMetadataPerms2: fixVideoMetadataPerms2,
|
||||
deleteJSONFile: deleteJSONFile,
|
||||
deleteJSONFile2: deleteJSONFile2,
|
||||
removeIDFromArchive, removeIDFromArchive,
|
||||
removeIDFromArchive: removeIDFromArchive,
|
||||
getDownloadedFilesByType: getDownloadedFilesByType,
|
||||
createContainerZipFile: createContainerZipFile,
|
||||
durationStringToNumber: durationStringToNumber,
|
||||
getMatchingCategoryFiles: getMatchingCategoryFiles,
|
||||
addUIDsToCategory: addUIDsToCategory,
|
||||
getCurrentDownloader: getCurrentDownloader,
|
||||
recFindByExt: recFindByExt,
|
||||
removeFileExtension: removeFileExtension,
|
||||
formatDateString: formatDateString,
|
||||
cropFile: cropFile,
|
||||
createEdgeNGrams: createEdgeNGrams,
|
||||
wait: wait,
|
||||
File: File
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ services:
|
||||
ytdl_material:
|
||||
environment:
|
||||
ALLOW_CONFIG_MUTATIONS: 'true'
|
||||
ytdl_mongodb_connection_string: 'mongodb://ytdl-mongo-db:27018'
|
||||
ytdl_mongodb_connection_string: 'mongodb://ytdl-mongo-db:27017'
|
||||
ytdl_use_local_db: 'false'
|
||||
write_ytdl_config: 'true'
|
||||
restart: always
|
||||
volumes:
|
||||
- ./appdata:/app/appdata
|
||||
@@ -14,11 +15,11 @@ services:
|
||||
- ./users:/app/users
|
||||
ports:
|
||||
- "8998:17442"
|
||||
image: tzahi12345/youtubedl-material:latest
|
||||
image: tzahi12345/youtubedl-material:nightly
|
||||
ytdl-mongo-db:
|
||||
image: mongo
|
||||
ports:
|
||||
- "27018:27017"
|
||||
- "27017:27017"
|
||||
logging:
|
||||
driver: "none"
|
||||
container_name: mongo-db
|
||||
|
||||
986
package-lock.json
generated
986
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@
|
||||
"@ngneat/content-loader": "^5.0.0",
|
||||
"@videogular/ngx-videogular": "^2.1.0",
|
||||
"core-js": "^2.4.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"filesize": "^6.1.0",
|
||||
"fingerprintjs2": "^2.1.0",
|
||||
@@ -57,8 +58,11 @@
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"codelyzer": "^6.0.0",
|
||||
"electron": "^8.0.1",
|
||||
"electron": "^9.4.0",
|
||||
"eslint": "^7.32.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.0.0",
|
||||
|
||||
@@ -7,14 +7,16 @@ import { SubscriptionComponent } from './subscription/subscription/subscription.
|
||||
import { PostsService } from './posts.services';
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { DownloadsComponent } from './components/downloads/downloads.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'home', component: MainComponent, canActivate: [PostsService] },
|
||||
{ path: 'player', component: PlayerComponent, canActivate: [PostsService]},
|
||||
{ path: 'subscriptions', component: SubscriptionsComponent, canActivate: [PostsService] },
|
||||
{ path: 'subscription', component: SubscriptionComponent, canActivate: [PostsService] },
|
||||
{ path: 'settings', component: SettingsComponent, canActivate: [PostsService] },
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: 'downloads', component: DownloadsComponent },
|
||||
{ path: 'downloads', component: DownloadsComponent, canActivate: [PostsService] },
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' }
|
||||
];
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
<span i18n="Dark mode toggle label">Dark</span>
|
||||
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
|
||||
</button>
|
||||
<button *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('settings')))" (click)="openSettingsDialog()" mat-menu-item>
|
||||
<!-- <button *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('settings')))" (click)="openSettingsDialog()" mat-menu-item>
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span i18n="Settings menu label">Settings</span>
|
||||
</button>
|
||||
</button> -->
|
||||
<button (click)="openAboutDialog()" mat-menu-item>
|
||||
<mat-icon>info</mat-icon>
|
||||
<span i18n="About menu label">About</span>
|
||||
@@ -42,10 +42,14 @@
|
||||
<mat-nav-list>
|
||||
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
|
||||
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
|
||||
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
||||
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
||||
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
|
||||
<a *ngIf="postsService.config && allowSubscriptions && postsService.hasPermission('subscriptions')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
||||
<a *ngIf="postsService.config && enableDownloadsManager && postsService.hasPermission('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 && postsService.hasPermission('settings')">
|
||||
<mat-divider></mat-divider>
|
||||
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/settings'><ng-container i18n="Settings menu label">Settings</ng-container></a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && postsService.hasPermission('subscriptions')">
|
||||
<mat-divider *ngIf="postsService.subscriptions.length > 0"></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>
|
||||
</ng-container>
|
||||
</mat-nav-list>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild, HostBinding, AfterViewInit } from '@angular/core';
|
||||
import {MatDialogRef} from '@angular/material/dialog';
|
||||
import {PostsService} from './posts.services';
|
||||
import {FileCardComponent} from './file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSidenav } from '@angular/material/sidenav';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
@@ -16,7 +13,6 @@ import 'rxjs/add/operator/filter'
|
||||
import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from './youtube-search.service';
|
||||
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
@@ -28,7 +24,11 @@ import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dial
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
styleUrls: ['./app.component.css'],
|
||||
providers: [{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
}]
|
||||
})
|
||||
export class AppComponent implements OnInit, AfterViewInit {
|
||||
|
||||
@@ -118,6 +118,10 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
this.postsService.reloadCategories();
|
||||
|
||||
this.postsService.getVersionInfo().subscribe(res => {
|
||||
this.postsService.version_info = res['version_info'];
|
||||
});
|
||||
}
|
||||
|
||||
// theme stuff
|
||||
|
||||
@@ -34,7 +34,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { AppComponent } from './app.component';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { FileCardComponent } from './file-card/file-card.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { MainComponent } from './main/main.component';
|
||||
@@ -87,6 +86,7 @@ import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.compon
|
||||
import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.component';
|
||||
import { H401Interceptor } from './http.interceptor';
|
||||
import { ConcurrentStreamComponent } from './components/concurrent-stream/concurrent-stream.component';
|
||||
import { SkipAdButtonComponent } from './components/skip-ad-button/skip-ad-button.component';
|
||||
|
||||
registerLocaleData(es, 'es');
|
||||
|
||||
@@ -97,7 +97,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
FileCardComponent,
|
||||
MainComponent,
|
||||
PlayerComponent,
|
||||
InputDialogComponent,
|
||||
@@ -136,7 +135,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
EditCategoryDialogComponent,
|
||||
TwitchChatComponent,
|
||||
SeeMoreComponent,
|
||||
ConcurrentStreamComponent
|
||||
ConcurrentStreamComponent,
|
||||
SkipAdButtonComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -24,11 +24,18 @@ export class CustomPlaylistsComponent implements OnInit {
|
||||
this.getAllPlaylists();
|
||||
}
|
||||
});
|
||||
|
||||
this.postsService.playlists_changed.subscribe(changed => {
|
||||
if (changed) {
|
||||
this.getAllPlaylists();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAllPlaylists() {
|
||||
this.playlists_received = false;
|
||||
this.postsService.getAllFiles().subscribe(res => {
|
||||
// must call getAllFiles as we need to get category playlists as well
|
||||
this.postsService.getPlaylists().subscribe(res => {
|
||||
this.playlists = res['playlists'];
|
||||
this.playlists_received = true;
|
||||
});
|
||||
|
||||
@@ -1,27 +1,91 @@
|
||||
<div style="padding: 20px;">
|
||||
<div *ngFor="let session_downloads of downloads">
|
||||
<ng-container *ngIf="keys(session_downloads).length > 2">
|
||||
<mat-card style="padding-bottom: 30px; margin-bottom: 15px;">
|
||||
<h4 style="text-align: center;"><ng-container i18n="Session ID">Session ID:</ng-container> {{session_downloads['session_id']}}
|
||||
<span *ngIf="session_downloads['session_id'] === postsService.session_id"> <ng-container i18n="Current session">(current)</ng-container></span>
|
||||
</h4>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div *ngFor="let download of session_downloads | keyvalue: sort_downloads; let i = index;" class="col-12 my-1">
|
||||
<mat-card *ngIf="download.key !== 'session_id' && download.key !== '_id' && download.value" class="mat-elevation-z3">
|
||||
<app-download-item [download]="download.value" [queueNumber]="i+1" (cancelDownload)="clearDownload(session_downloads['session_id'], download.value.uid)"></app-download-item>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button style="top: 15px;" (click)="clearDownloads(session_downloads['session_id'])" mat-stroked-button color="warn"><ng-container i18n="clear all downloads action button">Clear all downloads</ng-container></button>
|
||||
</div>
|
||||
</mat-card>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div [hidden]="!(downloads && downloads.length > 0)">
|
||||
<div style="overflow: hidden;" [ngClass]="uids ? 'rounded mat-elevation-z2' : 'mat-elevation-z8'">
|
||||
<mat-table style="overflow: hidden" [ngClass]="uids ? 'rounded-top' : null" matSort [dataSource]="dataSource">
|
||||
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Date">Date</ng-container> </mat-header-cell>
|
||||
<mat-cell *matCellDef="let element"> {{element.timestamp_start | date: 'short'}} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Title Column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
<span class="one-line" [matTooltip]="element.title ? element.title : null">
|
||||
{{element.title}}
|
||||
</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="downloads && !downloadsValid()">
|
||||
<h4 style="text-align: center;" i18n="No downloads label">No downloads available!</h4>
|
||||
</div>
|
||||
<!-- Subscription Column -->
|
||||
<ng-container matColumnDef="subscription">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Subscription">Subscription</ng-container> </mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
<ng-container *ngIf="element.sub_name">
|
||||
{{element.sub_name}}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!element.sub_name">
|
||||
N/A
|
||||
</ng-container>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Stage Column -->
|
||||
<ng-container matColumnDef="stage">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Stage">Stage</ng-container> </mat-header-cell>
|
||||
<mat-cell *matCellDef="let element"> {{STEP_INDEX_TO_LABEL[element.step_index]}} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Progress Column -->
|
||||
<ng-container matColumnDef="progress">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Progress">Progress</ng-container> </mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
<ng-container *ngIf="element.percent_complete">
|
||||
{{+(element.percent_complete) > 100 ? '100' : element.percent_complete}}%
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!element.percent_complete">
|
||||
N/A
|
||||
</ng-container>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
<div>
|
||||
<ng-container *ngIf="!element.finished">
|
||||
<button (click)="pauseDownload(element.uid)" *ngIf="!element.paused || !element.finished_step" [disabled]="element.paused && !element.finished_step" mat-icon-button matTooltip="Pause" i18n-matTooltip="Pause"><mat-spinner [diameter]="28" *ngIf="element.paused && !element.finished_step" class="icon-button-spinner"></mat-spinner><mat-icon>pause</mat-icon></button>
|
||||
<button (click)="resumeDownload(element.uid)" *ngIf="element.paused && element.finished_step" mat-icon-button matTooltip="Resume" i18n-matTooltip="Resume"><mat-icon>play_arrow</mat-icon></button>
|
||||
<button *ngIf="false && !element.paused" (click)="cancelDownload(element.uid)" mat-icon-button matTooltip="Cancel" i18n-matTooltip="Cancel"><mat-icon>cancel</mat-icon></button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="element.finished">
|
||||
<button *ngIf="!element.error" (click)="watchContent(element)" mat-icon-button matTooltip="Watch content" i18n-matTooltip="Watch content"><mat-icon>smart_display</mat-icon></button>
|
||||
<button *ngIf="element.error" (click)="showError(element)" mat-icon-button matTooltip="Show error" i18n-matTooltip="Show error"><mat-icon>warning</mat-icon></button>
|
||||
<button (click)="restartDownload(element.uid)" mat-icon-button matTooltip="Restart" i18n-matTooltip="Restart"><mat-icon>restart_alt</mat-icon></button>
|
||||
</ng-container>
|
||||
<button *ngIf="element.finished || element.paused" (click)="clearDownload(element.uid)" mat-icon-button matTooltip="Clear" i18n-matTooltip="Clear"><mat-icon>delete</mat-icon></button>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row [ngClass]="uids ? 'rounded-top' : null" *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator [ngClass]="uids ? 'rounded-bottom' : null" [pageSizeOptions]="[5, 10, 20]"
|
||||
showFirstLastButtons
|
||||
aria-label="Select page of downloads">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
<div *ngIf="!uids" class="downloads-action-button-div">
|
||||
<button [disabled]="!running_download_exists" mat-stroked-button (click)="pauseAllDownloads()"><ng-container i18n="Pause all downloads">Pause all downloads</ng-container></button>
|
||||
<button style="margin-left: 10px;" [disabled]="!paused_download_exists" mat-stroked-button (click)="resumeAllDownloads()"><ng-container i18n="Resume all downloads">Resume all downloads</ng-container></button>
|
||||
<button color="warn" style="margin-left: 10px;" mat-stroked-button (click)="clearFinishedDownloads()"><ng-container i18n="Clear finished downloads">Clear finished downloads</ng-container></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="(!downloads || downloads.length === 0) && downloads_retrieved && !uids">
|
||||
<h4 style="text-align: center; margin-top: 10px;" i18n="No downloads label">No downloads available!</h4>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
mat-header-cell, mat-cell {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.one-line {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon-button-spinner {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.downloads-action-button-div {
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.rounded-top {
|
||||
border-radius: 16px 16px 0px 0px !important;
|
||||
}
|
||||
|
||||
.rounded-bottom {
|
||||
border-radius: 0px 0px 16px 16px !important;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 16px 16px 16px 16px !important;
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Component, OnInit, ViewChildren, QueryList, ElementRef, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ViewChild, Input, EventEmitter } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { trigger, transition, animateChild, stagger, query, style, animate } from '@angular/animations';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
|
||||
@Component({
|
||||
selector: 'app-downloads',
|
||||
@@ -34,138 +40,222 @@ import { Router } from '@angular/router';
|
||||
})
|
||||
export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() uids = null;
|
||||
|
||||
downloads_check_interval = 1000;
|
||||
downloads = [];
|
||||
finished_downloads = [];
|
||||
interval_id = null;
|
||||
|
||||
keys = Object.keys;
|
||||
|
||||
valid_sessions_length = 0;
|
||||
|
||||
paused_download_exists = false;
|
||||
running_download_exists = false;
|
||||
|
||||
STEP_INDEX_TO_LABEL = {
|
||||
0: $localize`Creating download`,
|
||||
1: $localize`Getting info`,
|
||||
2: $localize`Downloading file`,
|
||||
3: $localize`Complete`
|
||||
}
|
||||
|
||||
displayedColumns: string[] = ['date', 'title', 'stage', 'subscription', 'progress', 'actions'];
|
||||
dataSource = null; // new MatTableDataSource<Download>();
|
||||
downloads_retrieved = false;
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
sort_downloads = (a, b) => {
|
||||
const result = b.value.timestamp_start - a.value.timestamp_start;
|
||||
const result = b.timestamp_start - a.timestamp_start;
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(public postsService: PostsService, private router: Router) { }
|
||||
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog, private clipboard: Clipboard) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.postsService.initialized) {
|
||||
this.getCurrentDownloadsRecurring();
|
||||
} else {
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
this.getCurrentDownloadsRecurring();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentDownloadsRecurring(): void {
|
||||
if (!this.postsService.config['Extra']['enable_downloads_manager']) {
|
||||
this.router.navigate(['/home']);
|
||||
return;
|
||||
}
|
||||
this.getCurrentDownloads();
|
||||
this.interval_id = setInterval(() => {
|
||||
this.getCurrentDownloads();
|
||||
}, this.downloads_check_interval);
|
||||
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
if (!this.postsService.config['Extra']['enable_downloads_manager']) {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
ngOnDestroy(): void {
|
||||
if (this.interval_id) { clearInterval(this.interval_id) }
|
||||
}
|
||||
|
||||
getCurrentDownloads() {
|
||||
this.postsService.getCurrentDownloads().subscribe(res => {
|
||||
if (res['downloads']) {
|
||||
this.assignNewValues(res['downloads']);
|
||||
getCurrentDownloads(): void {
|
||||
this.postsService.getCurrentDownloads(this.uids).subscribe(res => {
|
||||
this.downloads_retrieved = true;
|
||||
if (res['downloads'] !== null
|
||||
&& res['downloads'] !== undefined
|
||||
&& JSON.stringify(this.downloads) !== JSON.stringify(res['downloads'])) {
|
||||
this.downloads = this.combineDownloads(this.downloads, res['downloads']);
|
||||
// this.downloads = res['downloads'];
|
||||
this.downloads.sort(this.sort_downloads);
|
||||
this.dataSource = new MatTableDataSource<Download>(this.downloads);
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
|
||||
this.paused_download_exists = this.downloads.find(download => download['paused'] && !download['error']);
|
||||
this.running_download_exists = this.downloads.find(download => !download['paused'] && !download['finished']);
|
||||
} else {
|
||||
// failed to get downloads
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearDownload(session_id, download_uid) {
|
||||
this.postsService.clearDownloads(false, session_id, download_uid).subscribe(res => {
|
||||
if (res['success']) {
|
||||
// this.downloads = res['downloads'];
|
||||
} else {
|
||||
clearFinishedDownloads(): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
dialogTitle: $localize`Clear finished downloads`,
|
||||
dialogText: $localize`Would you like to clear your finished downloads?`,
|
||||
submitText: $localize`Clear`,
|
||||
warnSubmitColor: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearDownloads(session_id) {
|
||||
this.postsService.clearDownloads(false, session_id).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.downloads = res['downloads'];
|
||||
} else {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearAllDownloads() {
|
||||
this.postsService.clearDownloads(true).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.downloads = res['downloads'];
|
||||
} else {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assignNewValues(new_downloads_by_session) {
|
||||
const session_keys = Object.keys(new_downloads_by_session);
|
||||
|
||||
// remove missing session IDs
|
||||
const current_session_ids = Object.keys(this.downloads);
|
||||
const missing_session_ids = current_session_ids.filter(session => session_keys.indexOf(session) === -1)
|
||||
|
||||
for (const missing_session_id of missing_session_ids) {
|
||||
delete this.downloads[missing_session_id];
|
||||
}
|
||||
|
||||
// loop through sessions
|
||||
for (let i = 0; i < session_keys.length; i++) {
|
||||
const session_id = session_keys[i];
|
||||
const session_downloads_by_id = new_downloads_by_session[session_id];
|
||||
const session_download_ids = Object.keys(session_downloads_by_id);
|
||||
|
||||
if (this.downloads[session_id]) {
|
||||
// remove missing download IDs
|
||||
const current_download_ids = Object.keys(this.downloads[session_id]);
|
||||
const missing_download_ids = current_download_ids.filter(download => session_download_ids.indexOf(download) === -1)
|
||||
|
||||
for (const missing_download_id of missing_download_ids) {
|
||||
console.log('removing missing download id');
|
||||
delete this.downloads[session_id][missing_download_id];
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.downloads[session_id]) {
|
||||
this.downloads[session_id] = session_downloads_by_id;
|
||||
} else {
|
||||
for (let j = 0; j < session_download_ids.length; j++) {
|
||||
if (session_download_ids[j] === 'session_id' || session_download_ids[j] === '_id') continue;
|
||||
const download_id = session_download_ids[j];
|
||||
const download = new_downloads_by_session[session_id][download_id]
|
||||
if (!this.downloads[session_id][download_id]) {
|
||||
this.downloads[session_id][download_id] = download;
|
||||
} else {
|
||||
const download_to_update = this.downloads[session_id][download_id];
|
||||
download_to_update['percent_complete'] = download['percent_complete'];
|
||||
download_to_update['complete'] = download['complete'];
|
||||
download_to_update['timestamp_end'] = download['timestamp_end'];
|
||||
download_to_update['downloading'] = download['downloading'];
|
||||
download_to_update['error'] = download['error'];
|
||||
dialogRef.afterClosed().subscribe(confirmed => {
|
||||
if (confirmed) {
|
||||
this.postsService.clearFinishedDownloads().subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to clear finished downloads!');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pauseDownload(download_uid: string): void {
|
||||
this.postsService.pauseDownload(download_uid).subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to pause download! See server logs for more info.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pauseAllDownloads(): void {
|
||||
this.postsService.pauseAllDownloads().subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to pause all downloads! See server logs for more info.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resumeDownload(download_uid: string): void {
|
||||
this.postsService.resumeDownload(download_uid).subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to resume download! See server logs for more info.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resumeAllDownloads(): void {
|
||||
this.postsService.resumeAllDownloads().subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to resume all downloads! See server logs for more info.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
restartDownload(download_uid: string): void {
|
||||
this.postsService.restartDownload(download_uid).subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to restart download! See server logs for more info.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cancelDownload(download_uid: string): void {
|
||||
this.postsService.cancelDownload(download_uid).subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to cancel download! See server logs for more info.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearDownload(download_uid: string): void {
|
||||
this.postsService.clearDownload(download_uid).subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to pause download! See server logs for more info.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watchContent(download): void {
|
||||
const container = download['container'];
|
||||
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
||||
const is_playlist = container['uids']; // hacky, TODO: fix
|
||||
if (is_playlist) {
|
||||
this.router.navigate(['/player', {playlist_id: container['id'], type: download['type']}]);
|
||||
} else {
|
||||
this.router.navigate(['/player', {type: download['type'], uid: container['uid']}]);
|
||||
}
|
||||
}
|
||||
|
||||
downloadsValid() {
|
||||
let valid = false;
|
||||
for (let i = 0; i < this.downloads.length; i++) {
|
||||
const session_downloads = this.downloads[i];
|
||||
if (!session_downloads) continue;
|
||||
if (this.keys(session_downloads).length > 2) {
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
combineDownloads(downloads_old, downloads_new) {
|
||||
// only keeps downloads that exist in the new set
|
||||
downloads_old = downloads_old.filter(download_old => downloads_new.some(download_new => download_new.uid === download_old.uid));
|
||||
|
||||
// add downloads from the new set that the old one doesn't have
|
||||
const downloads_to_add = downloads_new.filter(download_new => !downloads_old.some(download_old => download_new.uid === download_old.uid));
|
||||
downloads_old.push(...downloads_to_add);
|
||||
downloads_old.forEach(download_old => {
|
||||
const download_new = downloads_new.find(download_to_check => download_old.uid === download_to_check.uid);
|
||||
Object.keys(download_new).forEach(key => {
|
||||
download_old[key] = download_new[key];
|
||||
});
|
||||
|
||||
Object.keys(download_old).forEach(key => {
|
||||
if (!download_new[key]) delete download_old[key];
|
||||
});
|
||||
});
|
||||
|
||||
return downloads_old;
|
||||
}
|
||||
|
||||
showError(download) {
|
||||
const copyToClipboardEmitter = new EventEmitter<boolean>();
|
||||
this.dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
dialogTitle: $localize`Error for ${download['url']}:url:`,
|
||||
dialogText: download['error'],
|
||||
submitText: $localize`Copy to clipboard`,
|
||||
cancelText: $localize`Close`,
|
||||
closeOnSubmit: false,
|
||||
onlyEmitOnDone: true,
|
||||
doneEmitter: copyToClipboardEmitter
|
||||
}
|
||||
});
|
||||
copyToClipboardEmitter.subscribe(done => {
|
||||
if (done) {
|
||||
this.postsService.openSnackBar($localize`Copied to clipboard!`);
|
||||
this.clipboard.copy(download['error']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface Download {
|
||||
timestamp_start: number;
|
||||
title: string;
|
||||
step_index: number;
|
||||
progress: string;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<mat-card class="login-card">
|
||||
<mat-tab-group [(selectedIndex)]="selectedTabIndex">
|
||||
<mat-tab-group style="margin-bottom: 20px" [(selectedIndex)]="selectedTabIndex">
|
||||
<mat-tab label="Login">
|
||||
<div style="margin-top: 10px;">
|
||||
<mat-form-field>
|
||||
@@ -11,9 +11,6 @@
|
||||
<input [(ngModel)]="loginPasswordInput" (keyup.enter)="login()" type="password" matInput placeholder="Password">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px; margin-top: 10px;">
|
||||
<button [disabled]="loggingIn" color="primary" (click)="login()" mat-raised-button><ng-container i18n="Login">Login</ng-container></button>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab *ngIf="registrationEnabled" label="Register">
|
||||
<div style="margin-top: 10px;">
|
||||
@@ -31,9 +28,14 @@
|
||||
<input [(ngModel)]="registrationPasswordConfirmationInput" type="password" matInput placeholder="Confirm Password">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px; margin-top: 10px;">
|
||||
<button [disabled]="registering" color="primary" (click)="register()" mat-raised-button><ng-container i18n="Register">Register</ng-container></button>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<div *ngIf="selectedTabIndex === 0" class="login-button-div">
|
||||
<button [disabled]="loggingIn" color="primary" (click)="login()" mat-raised-button><ng-container i18n="Login">Login</ng-container></button>
|
||||
<mat-progress-bar *ngIf="loggingIn" class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<div *ngIf="selectedTabIndex === 1" class="login-button-div">
|
||||
<button [disabled]="registering" color="primary" (click)="register()" mat-raised-button><ng-container i18n="Register">Register</ng-container></button>
|
||||
<mat-progress-bar *ngIf="registering" class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
</mat-card>
|
||||
@@ -1,6 +1,33 @@
|
||||
.login-card {
|
||||
max-width: 600px;
|
||||
max-width: 400px;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
margin-top: 20px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.login-div {
|
||||
height: calc(100% - 170px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.login-button-div {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.login-button-div > button {
|
||||
width: 100%;
|
||||
border-radius: 0px 0px 4px 4px !important;
|
||||
}
|
||||
|
||||
.login-progress-bar {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<div style="height: 275px;">
|
||||
<div style="height: 100%;">
|
||||
<div *ngIf="logs_loading" style="z-index: 999; position: absolute; top: 40%; left: 50%">
|
||||
<mat-spinner [diameter]="32"></mat-spinner>
|
||||
</div>
|
||||
@@ -10,7 +10,7 @@
|
||||
</cdk-virtual-scroll-viewport>-->
|
||||
|
||||
<!-- Non-virtual mode (slow, bug-free) -->
|
||||
<div style="height: 274px; overflow-y: auto">
|
||||
<div style="height: 100%; overflow-y: auto">
|
||||
<div *ngFor="let log of logs; let i = index" class="example-item">
|
||||
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div *ngIf="dataSource; else loading">
|
||||
<div style="padding: 15px">
|
||||
<div class="row">
|
||||
<div class="table table-responsive px-5 pb-4 pt-2">
|
||||
<div class="table table-responsive pb-4 pt-2">
|
||||
<div class="example-header">
|
||||
<mat-form-field>
|
||||
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search" i18n-placeholder="search field description">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.edit-role {
|
||||
position: relative;
|
||||
top: -50px;
|
||||
left: 35px;
|
||||
}
|
||||
@@ -28,13 +28,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="container">
|
||||
<div class="container" style="margin-bottom: 16px">
|
||||
<div class="row justify-content-center">
|
||||
<ng-container *ngIf="normal_files_received && paged_data">
|
||||
<div *ngFor="let file of paged_data; 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" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? '?jwt=' + this.postsService.token : ''"></app-unified-file-card>
|
||||
</div>
|
||||
<div *ngIf="filtered_files.length === 0">
|
||||
<div *ngIf="paged_data.length === 0">
|
||||
<ng-container i18n="No videos found">No videos found.</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -46,8 +46,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-paginator class="paginator" #paginator *ngIf="filtered_files && filtered_files.length > 0" (page)="pageChangeEvent($event)" [length]="filtered_files.length"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="[5, 10, 25, 100, this.paged_data && this.paged_data.length > 100 ? this.paged_data.length : 250]">
|
||||
</mat-paginator>
|
||||
<div>
|
||||
<div style="position: absolute; margin-left: -8px; margin-top: 5px; scale: 0.8">
|
||||
<mat-form-field>
|
||||
<mat-label><ng-container i18n="File type">File type</ng-container></mat-label>
|
||||
<mat-select color="accent" [(ngModel)]="fileTypeFilter" (selectionChange)="fileTypeFilterChanged($event.value)">
|
||||
<mat-option value="both"><ng-container i18n="Both">Both</ng-container></mat-option>
|
||||
<mat-option value="video_only"><ng-container i18n="Video only">Video only</ng-container></mat-option>
|
||||
<mat-option value="audio_only"><ng-container i18n="Audio only">Audio only</ng-container></mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<mat-paginator class="paginator" #paginator *ngIf="paged_data && paged_data.length > 0" (page)="pageChangeEvent($event)" [length]="file_count"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="[5, 10, 25, 100, this.paged_data && this.paged_data.length > 100 ? this.paged_data.length : 250]">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,6 +2,8 @@ import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { Subject } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recent-videos',
|
||||
@@ -15,8 +17,8 @@ export class RecentVideosComponent implements OnInit {
|
||||
|
||||
normal_files_received = false;
|
||||
subscription_files_received = false;
|
||||
files: any[] = null;
|
||||
filtered_files: any[] = null;
|
||||
file_count = 10;
|
||||
searchChangedSubject: Subject<string> = new Subject<string>();
|
||||
downloading_content = {'video': {}, 'audio': {}};
|
||||
search_mode = false;
|
||||
search_text = '';
|
||||
@@ -50,6 +52,9 @@ export class RecentVideosComponent implements OnInit {
|
||||
}
|
||||
};
|
||||
filterProperty = this.filterProperties['upload_date'];
|
||||
fileTypeFilter = 'both';
|
||||
|
||||
playlists = null;
|
||||
|
||||
pageSize = 10;
|
||||
paged_data = null;
|
||||
@@ -68,83 +73,108 @@ export class RecentVideosComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
if (this.postsService.initialized) {
|
||||
this.getAllFiles();
|
||||
this.getAllPlaylists();
|
||||
}
|
||||
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
this.getAllFiles();
|
||||
this.getAllPlaylists();
|
||||
}
|
||||
});
|
||||
|
||||
this.postsService.files_changed.subscribe(changed => {
|
||||
if (changed) {
|
||||
this.getAllFiles();
|
||||
}
|
||||
});
|
||||
|
||||
// set filter property to cached
|
||||
this.postsService.playlists_changed.subscribe(changed => {
|
||||
if (changed) {
|
||||
this.getAllPlaylists();
|
||||
}
|
||||
});
|
||||
|
||||
// set filter property to cached value
|
||||
const cached_filter_property = localStorage.getItem('filter_property');
|
||||
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
|
||||
this.filterProperty = this.filterProperties[cached_filter_property];
|
||||
}
|
||||
|
||||
// set file type filter to cached value
|
||||
const cached_file_type_filter = localStorage.getItem('file_type_filter');
|
||||
if (cached_file_type_filter) {
|
||||
this.fileTypeFilter = cached_file_type_filter;
|
||||
}
|
||||
|
||||
const sort_order = localStorage.getItem('recent_videos_sort_order');
|
||||
|
||||
if (sort_order) {
|
||||
this.descendingMode = sort_order === 'descending';
|
||||
}
|
||||
|
||||
this.searchChangedSubject
|
||||
.debounceTime(500)
|
||||
.pipe(distinctUntilChanged()
|
||||
).subscribe(model => {
|
||||
if (model.length > 0) {
|
||||
this.search_mode = true;
|
||||
} else {
|
||||
this.search_mode = false;
|
||||
}
|
||||
this.getAllFiles();
|
||||
});
|
||||
}
|
||||
|
||||
getAllPlaylists() {
|
||||
this.postsService.getPlaylists().subscribe(res => {
|
||||
this.playlists = res['playlists'];
|
||||
});
|
||||
}
|
||||
|
||||
// search
|
||||
|
||||
onSearchInputChanged(newvalue) {
|
||||
if (newvalue.length > 0) {
|
||||
this.search_mode = true;
|
||||
this.filterFiles(newvalue);
|
||||
} else {
|
||||
this.search_mode = false;
|
||||
this.filtered_files = this.files;
|
||||
}
|
||||
}
|
||||
|
||||
private filterFiles(value: string) {
|
||||
const filterValue = value.toLowerCase();
|
||||
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue) || option.category?.name?.toLowerCase().includes(filterValue));
|
||||
this.pageChangeEvent({pageSize: this.pageSize, pageIndex: this.paginator.pageIndex});
|
||||
}
|
||||
|
||||
filterByProperty(prop) {
|
||||
if (this.descendingMode) {
|
||||
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? -1 : 1));
|
||||
} else {
|
||||
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? 1 : -1));
|
||||
}
|
||||
if (this.paginator) { this.pageChangeEvent({pageSize: this.pageSize, pageIndex: this.paginator.pageIndex}) };
|
||||
this.normal_files_received = false;
|
||||
this.searchChangedSubject.next(newvalue);
|
||||
}
|
||||
|
||||
filterOptionChanged(value) {
|
||||
this.filterByProperty(value['property']);
|
||||
localStorage.setItem('filter_property', value['key']);
|
||||
this.getAllFiles();
|
||||
}
|
||||
|
||||
fileTypeFilterChanged(value) {
|
||||
localStorage.setItem('file_type_filter', value);
|
||||
this.getAllFiles();
|
||||
}
|
||||
|
||||
toggleModeChange() {
|
||||
this.descendingMode = !this.descendingMode;
|
||||
this.filterByProperty(this.filterProperty['property']);
|
||||
localStorage.setItem('recent_videos_sort_order', this.descendingMode ? 'descending' : 'ascending');
|
||||
this.getAllFiles();
|
||||
}
|
||||
|
||||
// get files
|
||||
|
||||
getAllFiles() {
|
||||
this.normal_files_received = false;
|
||||
this.postsService.getAllFiles().subscribe(res => {
|
||||
this.files = res['files'];
|
||||
this.files.sort(this.sortFiles);
|
||||
for (let i = 0; i < this.files.length; i++) {
|
||||
const file = this.files[i];
|
||||
getAllFiles(cache_mode = false) {
|
||||
this.normal_files_received = cache_mode;
|
||||
const current_file_index = (this.paginator?.pageIndex ? this.paginator.pageIndex : 0)*this.pageSize;
|
||||
const sort = {by: this.filterProperty['property'], order: this.descendingMode ? -1 : 1};
|
||||
const range = [current_file_index, current_file_index + this.pageSize];
|
||||
this.postsService.getAllFiles(sort, range, this.search_mode ? this.search_text : null, this.fileTypeFilter).subscribe(res => {
|
||||
this.file_count = res['file_count'];
|
||||
this.paged_data = res['files'];
|
||||
for (let i = 0; i < this.paged_data.length; i++) {
|
||||
const file = this.paged_data[i];
|
||||
file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration);
|
||||
}
|
||||
if (this.search_mode) {
|
||||
this.filterFiles(this.search_text);
|
||||
} else {
|
||||
this.filtered_files = this.files;
|
||||
}
|
||||
this.filterByProperty(this.filterProperty['property']);
|
||||
|
||||
// set cached file count for future use, note that we convert the amount of files to a string
|
||||
localStorage.setItem('cached_file_count', '' + this.files.length);
|
||||
localStorage.setItem('cached_file_count', '' + this.file_count);
|
||||
|
||||
this.normal_files_received = true;
|
||||
|
||||
this.paged_data = this.filtered_files.slice(0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,8 +202,8 @@ export class RecentVideosComponent implements OnInit {
|
||||
} else {
|
||||
// normal subscriptions
|
||||
!new_tab ? this.router.navigate(['/player', {uid: file.uid,
|
||||
type: file.isAudio ? 'audio' : 'video', sub_id: sub.id}])
|
||||
: window.open(`/#/player;uid=${file.uid};type=${file.isAudio ? 'audio' : 'video'};sub_id=${sub.id}`);
|
||||
type: file.isAudio ? 'audio' : 'video'}])
|
||||
: window.open(`/#/player;uid=${file.uid};type=${file.isAudio ? 'audio' : 'video'}`);
|
||||
}
|
||||
} else {
|
||||
// normal files
|
||||
@@ -280,12 +310,26 @@ export class RecentVideosComponent implements OnInit {
|
||||
}
|
||||
|
||||
removeFileCard(file_to_remove) {
|
||||
const index = this.files.map(e => e.uid).indexOf(file_to_remove.uid);
|
||||
this.files.splice(index, 1);
|
||||
if (this.search_mode) {
|
||||
this.filterFiles(this.search_text);
|
||||
}
|
||||
this.filterByProperty(this.filterProperty['property']);
|
||||
const index = this.paged_data.map(e => e.uid).indexOf(file_to_remove.uid);
|
||||
this.paged_data.splice(index, 1);
|
||||
this.getAllFiles(true);
|
||||
}
|
||||
|
||||
addFileToPlaylist(info_obj) {
|
||||
const file = info_obj['file'];
|
||||
const playlist_id = info_obj['playlist_id'];
|
||||
const playlist = this.playlists.find(potential_playlist => potential_playlist['id'] === playlist_id);
|
||||
this.postsService.addFileToPlaylist(playlist_id, file['uid']).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.postsService.openSnackBar(`Successfully added ${file.title} to ${playlist.title}!`);
|
||||
this.postsService.playlists_changed.next(true);
|
||||
} else {
|
||||
this.postsService.openSnackBar(`Failed to add ${file.title} to ${playlist.title}! Unknown error.`);
|
||||
}
|
||||
}, err => {
|
||||
console.error(err);
|
||||
this.postsService.openSnackBar(`Failed to add ${file.title} to ${playlist.title}! See browser console for error.`);
|
||||
});
|
||||
}
|
||||
|
||||
// sorting and filtering
|
||||
@@ -306,7 +350,8 @@ export class RecentVideosComponent implements OnInit {
|
||||
}
|
||||
|
||||
pageChangeEvent(event) {
|
||||
const offset = ((event.pageIndex + 1) - 1) * event.pageSize;
|
||||
this.paged_data = this.filtered_files.slice(offset).slice(0, event.pageSize);
|
||||
this.pageSize = event.pageSize;
|
||||
this.loading_files = Array(this.pageSize).fill(0);
|
||||
this.getAllFiles();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<button *ngIf="show_skip_ad_button" (click)="skipAdButtonClicked()" mat-flat-button><ng-container i18n="Skip ad button">Skip ad</ng-container></button>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SkipAdButtonComponent } from './skip-ad-button.component';
|
||||
|
||||
describe('SkipAdButtonComponent', () => {
|
||||
let component: SkipAdButtonComponent;
|
||||
let fixture: ComponentFixture<SkipAdButtonComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ SkipAdButtonComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SkipAdButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
115
src/app/components/skip-ad-button/skip-ad-button.component.ts
Normal file
115
src/app/components/skip-ad-button/skip-ad-button.component.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
@Component({
|
||||
selector: 'app-skip-ad-button',
|
||||
templateUrl: './skip-ad-button.component.html',
|
||||
styleUrls: ['./skip-ad-button.component.scss']
|
||||
})
|
||||
export class SkipAdButtonComponent implements OnInit {
|
||||
|
||||
@Input() current_video = null;
|
||||
@Input() playback_timestamp = null;
|
||||
|
||||
@Output() setPlaybackTimestamp = new EventEmitter<any>();
|
||||
|
||||
sponsor_block_cache = {};
|
||||
show_skip_ad_button = false;
|
||||
|
||||
skip_ad_button_check_interval = null;
|
||||
|
||||
constructor(private postsService: PostsService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.skip_ad_button_check_interval = setInterval(() => this.skipAdButtonCheck(), 500);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
clearInterval(this.skip_ad_button_check_interval);
|
||||
}
|
||||
|
||||
checkSponsorBlock(video_to_check) {
|
||||
if (!video_to_check) return;
|
||||
|
||||
// check cache, null means it has been checked and confirmed not to exist (limits API calls)
|
||||
if (this.sponsor_block_cache[video_to_check.url] || this.sponsor_block_cache[video_to_check.url] === null) return;
|
||||
|
||||
// sponsor block needs first 4 chars from video ID hash
|
||||
const video_id = this.getVideoIDFromURL(video_to_check.url);
|
||||
const id_hash = this.getVideoIDHashFromURL(video_id);
|
||||
if (!id_hash || id_hash.length < 4) return;
|
||||
const truncated_id_hash = id_hash.substring(0, 4);
|
||||
|
||||
// we couldn't get the data from the cache, let's get it from sponsor block directly
|
||||
|
||||
this.postsService.getSponsorBlockDataForVideo(truncated_id_hash).subscribe(res => {
|
||||
if (res && res['length'] && res['length'] === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const found_data = res['find'](data => data['videoID'] === video_id);
|
||||
if (found_data) {
|
||||
this.sponsor_block_cache[video_to_check.url] = found_data;
|
||||
} else {
|
||||
this.sponsor_block_cache[video_to_check.url] = null;
|
||||
}
|
||||
}, err => {
|
||||
// likely doesn't exist
|
||||
this.sponsor_block_cache[video_to_check.url] = null;
|
||||
});
|
||||
}
|
||||
|
||||
getVideoIDHashFromURL(video_id) {
|
||||
if (!video_id) return null;
|
||||
return CryptoJS.SHA256(video_id).toString(CryptoJS.enc.Hex);;
|
||||
}
|
||||
|
||||
getVideoIDFromURL(url) {
|
||||
const regex_exp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
|
||||
const match = url.match(regex_exp);
|
||||
return (match && match[7].length==11) ? match[7] : null;
|
||||
}
|
||||
|
||||
skipAdButtonCheck() {
|
||||
const sponsor_block_data = this.sponsor_block_cache[this.current_video.url];
|
||||
if (!sponsor_block_data && sponsor_block_data !== null) {
|
||||
// we haven't yet tried to get the sponsor block data for the video
|
||||
this.checkSponsorBlock(this.current_video);
|
||||
} else if (!sponsor_block_data) {
|
||||
this.show_skip_ad_button = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getTimeToSkipTo()) {
|
||||
this.show_skip_ad_button = true;
|
||||
} else {
|
||||
this.show_skip_ad_button = false;
|
||||
}
|
||||
}
|
||||
|
||||
getTimeToSkipTo() {
|
||||
const sponsor_block_data = this.sponsor_block_cache[this.current_video.url];
|
||||
|
||||
if (!sponsor_block_data) return;
|
||||
|
||||
// check if we're in between an ad segment
|
||||
const found_segment = sponsor_block_data['segments'].find(segment_data => this.playback_timestamp > segment_data.segment[0] && this.playback_timestamp < segment_data.segment[1] - 0.5);
|
||||
|
||||
if (found_segment) {
|
||||
return found_segment['segment'][1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
skipAdButtonClicked() {
|
||||
const time_to_skip_to = this.getTimeToSkipTo();
|
||||
if (!time_to_skip_to) return;
|
||||
|
||||
this.setPlaybackTimestamp.emit(time_to_skip_to);
|
||||
|
||||
this.show_skip_ad_button = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<div (mouseover)="elevated=true" (mouseout)="elevated=false" (contextmenu)="onRightClick($event)" style="position: relative; width: fit-content;">
|
||||
<div (mouseenter)="onMouseOver()" (mouseleave)="onMouseOut()" (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>
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
<ng-container *ngIf="!is_playlist && !loading">
|
||||
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
||||
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon> <ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
|
||||
<button *ngIf="availablePlaylists" [matMenuTriggerFor]="addtoplaylist" mat-menu-item><mat-icon>playlist_add</mat-icon> <ng-container i18n="Add to playlist menu item">Add to playlist</ng-container></button>
|
||||
<mat-menu #addtoplaylist="matMenu">
|
||||
<ng-container *ngFor="let playlist of availablePlaylists">
|
||||
<button *ngIf="(playlist.type === 'audio') === file_obj.isAudio" [disabled]="playlist.uids?.includes(file_obj.uid)" (click)="emitAddFileToPlaylist(playlist.id)" mat-menu-item>{{playlist.name}}</button>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
<mat-divider></mat-divider>
|
||||
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item>
|
||||
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
|
||||
@@ -45,8 +51,10 @@
|
||||
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
|
||||
<div style="padding:5px">
|
||||
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
|
||||
<div style="position: relative">
|
||||
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailPath ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
|
||||
<div [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" style="position: relative">
|
||||
<img *ngIf="!hide_image || is_playlist || (file_obj.type === 'audio' || file_obj.isAudio)" [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">
|
||||
<video *ngIf="elevated && !is_playlist && !(file_obj.type === 'audio' || file_obj.isAudio)" autoplay loop muted [muted]="true" [ngClass]="{'video-small': card_size === 'small', 'video': card_size === 'medium', 'video-large': card_size === 'large'}" [src]="streamURL">
|
||||
</video>
|
||||
<div class="duration-time">
|
||||
{{file_length}}
|
||||
</div>
|
||||
|
||||
@@ -51,6 +51,30 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-large {
|
||||
width: 300px;
|
||||
height: 167.5px;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.video {
|
||||
width: 200px;
|
||||
height: 112.5px;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.video-small {
|
||||
width: 150px;
|
||||
height: 84.5px;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.example-full-width-height {
|
||||
width: 100%;
|
||||
height: 100%
|
||||
|
||||
@@ -35,6 +35,9 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
// optional vars
|
||||
thumbnailBlobURL = null;
|
||||
|
||||
streamURL = null;
|
||||
hide_image = false;
|
||||
|
||||
// input/output
|
||||
@Input() loading = true;
|
||||
@Input() theme = null;
|
||||
@@ -46,9 +49,11 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
@Input() locale = null;
|
||||
@Input() baseStreamPath = null;
|
||||
@Input() jwtString = null;
|
||||
@Input() availablePlaylists = null;
|
||||
@Output() goToFile = new EventEmitter<any>();
|
||||
@Output() goToSubscription = new EventEmitter<any>();
|
||||
@Output() deleteFile = new EventEmitter<any>();
|
||||
@Output() addFileToPlaylist = new EventEmitter<any>();
|
||||
@Output() editPlaylist = new EventEmitter<any>();
|
||||
|
||||
|
||||
@@ -76,6 +81,8 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
const bloburl = URL.createObjectURL(blob);
|
||||
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);*/
|
||||
}
|
||||
|
||||
if (this.file_obj) this.streamURL = this.generateStreamURL();
|
||||
}
|
||||
|
||||
emitDeleteFile(blacklistMode = false) {
|
||||
@@ -86,6 +93,13 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
emitAddFileToPlaylist(playlist_id) {
|
||||
this.addFileToPlaylist.emit({
|
||||
file: this.file_obj,
|
||||
playlist_id: playlist_id
|
||||
});
|
||||
}
|
||||
|
||||
navigateToFile(event) {
|
||||
this.goToFile.emit({file: this.file_obj, event: event});
|
||||
}
|
||||
@@ -119,6 +133,33 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
this.contextMenu.openMenu();
|
||||
}
|
||||
|
||||
generateStreamURL() {
|
||||
let baseLocation = 'stream/';
|
||||
let fullLocation = this.baseStreamPath + baseLocation + `?test=test&uid=${this.file_obj['uid']}`;
|
||||
|
||||
if (this.jwtString) {
|
||||
fullLocation += `&jwt=${this.jwtString}`;
|
||||
}
|
||||
|
||||
fullLocation += '&t=,10';
|
||||
|
||||
return fullLocation;
|
||||
}
|
||||
|
||||
onMouseOver() {
|
||||
this.elevated = true;
|
||||
setTimeout(() => {
|
||||
if (this.elevated) {
|
||||
this.hide_image = true;
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
onMouseOut() {
|
||||
this.elevated = false;
|
||||
this.hide_image = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function fancyTimeFormat(time) {
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon> <ng-container *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag"><a [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container></ng-container>
|
||||
<span *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] === current_version_tag">You are up to date.</span>
|
||||
</p>
|
||||
<p>
|
||||
<ng-container i18n="Installation type">Installation type:</ng-container> {{postsService.version_info.type}}
|
||||
<br>
|
||||
<ng-container *ngIf="postsService.version_info.type === 'docker'">
|
||||
<ng-container i18n="Docker tag">Docker tag:</ng-container> {{postsService.version_info.tag}}
|
||||
<br>
|
||||
</ng-container>
|
||||
<ng-container i18n="Commit hash">Commit hash:</ng-container> {{postsService.version_info.commit}}
|
||||
<br>
|
||||
<ng-container i18n="Build date">Build date:</ng-container> {{postsService.version_info.date}}
|
||||
</p>
|
||||
<p>
|
||||
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container> <a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a> <ng-container i18n="About bug suffix">to create an issue!</ng-container>
|
||||
</p>
|
||||
|
||||
@@ -19,7 +19,7 @@ export class AboutDialogComponent implements OnInit {
|
||||
sidepanel_mode = this.postsService.sidepanel_mode;
|
||||
card_size = this.postsService.card_size;
|
||||
|
||||
constructor(private postsService: PostsService) { }
|
||||
constructor(public postsService: PostsService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getLatestGithubRelease();
|
||||
|
||||
@@ -11,5 +11,8 @@
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
<button style="float: right;" mat-stroked-button mat-dialog-close>Cancel</button>
|
||||
<button style="float: right;" mat-stroked-button mat-dialog-close>
|
||||
<ng-container *ngIf="cancelText">{{cancelText}}</ng-container>
|
||||
<ng-container *ngIf="!cancelText" i18n="Cancel">Cancel</ng-container>
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -11,18 +11,23 @@ export class ConfirmDialogComponent implements OnInit {
|
||||
dialogTitle = 'Confirm';
|
||||
dialogText = 'Would you like to confirm?';
|
||||
submitText = 'Yes'
|
||||
cancelText = null;
|
||||
submitClicked = false;
|
||||
closeOnSubmit = true;
|
||||
|
||||
doneEmitter: EventEmitter<any> = null;
|
||||
doneEmitter: EventEmitter<boolean> = 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 };
|
||||
if (this.data.dialogTitle !== undefined) { this.dialogTitle = this.data.dialogTitle }
|
||||
if (this.data.dialogText !== undefined) { this.dialogText = this.data.dialogText }
|
||||
if (this.data.submitText !== undefined) { this.submitText = this.data.submitText }
|
||||
if (this.data.cancelText !== undefined) { this.cancelText = this.data.cancelText }
|
||||
if (this.data.warnSubmitColor !== undefined) { this.warnSubmitColor = this.data.warnSubmitColor }
|
||||
if (this.data.warnSubmitColor !== undefined) { this.warnSubmitColor = this.data.warnSubmitColor }
|
||||
if (this.data.closeOnSubmit !== undefined) { this.closeOnSubmit = this.data.closeOnSubmit }
|
||||
|
||||
// checks if emitter exists, if so don't autoclose as it should be handled by caller
|
||||
if (this.data.doneEmitter) {
|
||||
@@ -34,9 +39,9 @@ export class ConfirmDialogComponent implements OnInit {
|
||||
confirmClicked() {
|
||||
if (this.onlyEmitOnDone) {
|
||||
this.doneEmitter.emit(true);
|
||||
this.submitClicked = true;
|
||||
if (this.closeOnSubmit) this.submitClicked = true;
|
||||
} else {
|
||||
this.dialogRef.close(true);
|
||||
if (this.closeOnSubmit) this.dialogRef.close(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-checkbox [(ngModel)]="playlist.randomize_order"><ng-container i18n="Randomize order when playing checkbox label">Randomize order when playing</ng-container></mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 10px; height: 40px;">
|
||||
<div style="float: left">
|
||||
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order </span>
|
||||
|
||||
@@ -57,6 +57,7 @@ export class ModifyPlaylistComponent implements OnInit {
|
||||
this.playlist_updated = true;
|
||||
this.postsService.openSnackBar('Playlist updated successfully.');
|
||||
this.getPlaylist();
|
||||
this.postsService.playlists_changed.next(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,6 +78,7 @@ export class ModifyPlaylistComponent implements OnInit {
|
||||
|
||||
addContent(file) {
|
||||
this.playlist_file_objs.push(file);
|
||||
this.playlist.uids.push(file.uid);
|
||||
this.processFiles();
|
||||
}
|
||||
|
||||
@@ -85,6 +87,7 @@ export class ModifyPlaylistComponent implements OnInit {
|
||||
index = this.playlist_file_objs.length - 1 - index;
|
||||
}
|
||||
this.playlist_file_objs.splice(index, 1);
|
||||
this.playlist.uids.splice(index, 1);
|
||||
this.processFiles();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
.example-card {
|
||||
width: 150px;
|
||||
height: 125px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
top:-5px;
|
||||
right:-5px;
|
||||
position:absolute;
|
||||
}
|
||||
|
||||
/* Coerce the <span> icon container away from display:inline */
|
||||
.mat-icon-button .mat-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.example-full-width-height {
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.img-div {
|
||||
height: 60px;
|
||||
padding: 0px;
|
||||
margin: 8px 0px 0px -5px;
|
||||
width: calc(100% + 5px + 5px);
|
||||
overflow: hidden;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
}
|
||||
|
||||
.max-two-lines {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
max-height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.file-link {
|
||||
width: 80%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 576px){
|
||||
|
||||
.example-card {
|
||||
width: 125px !important;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<mat-card class="example-card mat-elevation-z6">
|
||||
<div style="padding:5px">
|
||||
<div style="height: 52px;">
|
||||
<div>
|
||||
<b><a class="file-link" href="javascript:void(0)" (click)="!playlist ? mainComponent.goToFile(name, isAudio, uid) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
|
||||
</div>
|
||||
<span class="max-two-lines"><ng-container i18n="File or playlist ID">ID:</ng-container> {{name}}</span>
|
||||
<div *ngIf="playlist"><ng-container i18n="Playlist video count">Count:</ng-container> {{count}}</div>
|
||||
</div>
|
||||
<div *ngIf="!image_errored && thumbnailURL" class="img-div">
|
||||
<img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail">
|
||||
<span *ngIf="!image_loaded">
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button [matMenuTriggerFor]="playlist_menu" *ngIf="playlist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #playlist_menu="matMenu">
|
||||
<button (click)="editPlaylistDialog()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
|
||||
<button (click)="deleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
|
||||
</mat-menu>
|
||||
<button [matMenuTriggerFor]="action_menu" *ngIf="!playlist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #action_menu="matMenu">
|
||||
<button (click)="openVideoInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
||||
<button (click)="deleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
|
||||
<button *ngIf="use_youtubedl_archive" (click)="deleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
|
||||
</mat-menu>
|
||||
|
||||
</mat-card>
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { FileCardComponent } from './file-card.component';
|
||||
|
||||
describe('FileCardComponent', () => {
|
||||
let component: FileCardComponent;
|
||||
let fixture: ComponentFixture<FileCardComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ FileCardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FileCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
import { Component, OnInit, Input, Output } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import {EventEmitter} from '@angular/core';
|
||||
import { MainComponent } from 'app/main/main.component';
|
||||
import { Subject, Observable } from 'rxjs';
|
||||
import 'rxjs/add/observable/merge';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
||||
import { ModifyPlaylistComponent } from '../dialogs/modify-playlist/modify-playlist.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-card',
|
||||
templateUrl: './file-card.component.html',
|
||||
styleUrls: ['./file-card.component.css']
|
||||
})
|
||||
export class FileCardComponent implements OnInit {
|
||||
@Input() file: any;
|
||||
@Input() title: string;
|
||||
@Input() length: string;
|
||||
@Input() name: string;
|
||||
@Input() uid: string;
|
||||
@Input() thumbnailURL: string;
|
||||
@Input() isAudio = true;
|
||||
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Input() playlist = null;
|
||||
@Input() count = null;
|
||||
@Input() use_youtubedl_archive = false;
|
||||
type;
|
||||
image_loaded = false;
|
||||
image_errored = false;
|
||||
|
||||
scrollSubject;
|
||||
scrollAndLoad;
|
||||
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent,
|
||||
private dialog: MatDialog) {
|
||||
|
||||
this.scrollSubject = new Subject();
|
||||
this.scrollAndLoad = Observable.merge(
|
||||
Observable.fromEvent(window, 'scroll'),
|
||||
this.scrollSubject
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.type = this.isAudio ? 'audio' : 'video';
|
||||
|
||||
if (this.file && this.file.url && this.file.url.includes('youtu')) {
|
||||
const string_id = (this.playlist ? '?list=' : '?v=')
|
||||
const index_offset = (this.playlist ? 6 : 3);
|
||||
const end_index = this.file.url.indexOf(string_id) + index_offset;
|
||||
this.name = this.file.url.substring(end_index, this.file.url.length);
|
||||
}
|
||||
}
|
||||
|
||||
deleteFile(blacklistMode = false) {
|
||||
if (!this.playlist) {
|
||||
this.postsService.deleteFile(this.uid, blacklistMode).subscribe(result => {
|
||||
if (result) {
|
||||
this.openSnackBar('Delete success!', 'OK.');
|
||||
this.removeFile.emit(this.name);
|
||||
} else {
|
||||
this.openSnackBar('Delete failed!', 'OK.');
|
||||
}
|
||||
}, err => {
|
||||
this.openSnackBar('Delete failed!', 'OK.');
|
||||
});
|
||||
} else {
|
||||
this.removeFile.emit(this.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
openVideoInfoDialog() {
|
||||
const dialogRef = this.dialog.open(VideoInfoDialogComponent, {
|
||||
data: {
|
||||
file: this.file,
|
||||
},
|
||||
minWidth: '50vw'
|
||||
});
|
||||
}
|
||||
|
||||
editPlaylistDialog() {
|
||||
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
|
||||
data: {
|
||||
playlist_id: this.playlist.id,
|
||||
width: '65vw'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(res => {
|
||||
// updates playlist in file manager if it changed
|
||||
if (dialogRef.componentInstance.playlist_updated) {
|
||||
this.playlist = dialogRef.componentInstance.original_playlist;
|
||||
this.title = this.playlist.name;
|
||||
this.count = this.playlist.fileNames.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onImgError(event) {
|
||||
this.image_errored = true;
|
||||
}
|
||||
|
||||
onHoverResponse() {
|
||||
this.scrollSubject.next();
|
||||
}
|
||||
|
||||
imageLoaded(loaded) {
|
||||
this.image_loaded = true;
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -129,16 +129,22 @@ mat-form-field.mat-form-field {
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
margin-left: 10px;
|
||||
margin-left: 5px;
|
||||
margin-top: -6px;
|
||||
margin-bottom: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.border-radius-both {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.no-border-radius-bottom {
|
||||
border-radius: 4px 4px 0px 0px;
|
||||
border-radius: 16px 16px 0px 0px;
|
||||
}
|
||||
|
||||
.no-border-radius-top {
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
border-radius: 0px 0px 16px 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<br/>
|
||||
<div class="big demo-basic">
|
||||
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;" [ngClass]="(allowAdvancedDownload) ? 'no-border-radius-bottom' : null">
|
||||
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;" [ngClass]="(allowAdvancedDownload) ? 'no-border-radius-bottom' : 'border-radius-both'">
|
||||
<mat-card-content style="padding: 0px 8px 0px 8px;">
|
||||
<div style="position: relative; margin-right: 15px;">
|
||||
<form class="example-form">
|
||||
@@ -19,7 +19,7 @@
|
||||
Quality
|
||||
</ng-container>
|
||||
</mat-label>
|
||||
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
|
||||
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality" (ngModelChange)="argsChanged($event)">
|
||||
<mat-option [value]="''">
|
||||
Max
|
||||
</mat-option>
|
||||
@@ -65,9 +65,9 @@
|
||||
Only Audio
|
||||
</ng-container>
|
||||
</mat-checkbox>
|
||||
<mat-checkbox *ngIf="allowMultiDownloadMode" [disabled]="current_download" (change)="multiDownloadModeChanged($event)" [(ngModel)]="multiDownloadMode" style="float: right; margin-top: -12px">
|
||||
<ng-container i18n="Multi-download Mode checkbox">
|
||||
Multi-download Mode
|
||||
<mat-checkbox *ngIf="allowAutoplay" (change)="autoplayChanged($event)" [(ngModel)]="autoplay" style="float: right; margin-top: -12px">
|
||||
<ng-container i18n="Autoplay checkbox">
|
||||
Autoplay
|
||||
</ng-container>
|
||||
</mat-checkbox>
|
||||
|
||||
@@ -111,8 +111,13 @@
|
||||
</ng-container>
|
||||
</mat-checkbox>
|
||||
<button class="edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
|
||||
<mat-checkbox color="accent" [disabled]="!customArgsEnabled || current_download" (change)="replaceArgsChanged($event)" [(ngModel)]="replaceArgs" style="z-index: 999; margin-left: 10px" [ngModelOptions]="{standalone: true}">
|
||||
<ng-container i18n="Replace args">
|
||||
Replace args
|
||||
</ng-container>
|
||||
</mat-checkbox>
|
||||
<mat-form-field color="accent" style="margin-bottom: 42px;" class="advanced-input">
|
||||
<input [(ngModel)]="customArgs" [ngModelOptions]="{standalone: true}" [disabled]="!customArgsEnabled" matInput placeholder="Custom args" i18n-placeholder="Custom args placeholder">
|
||||
<input [(ngModel)]="customArgs" [ngModelOptions]="{standalone: true}" [disabled]="!customArgsEnabled" matInput (ngModelChange)="argsChanged()" placeholder="Custom args" i18n-placeholder="Custom args placeholder">
|
||||
<mat-hint>
|
||||
<ng-container i18n="Custom Args input hint">
|
||||
No need to include URL, just everything after. Args are delimited using two commas like so: ,,
|
||||
@@ -127,7 +132,7 @@
|
||||
</ng-container>
|
||||
</mat-checkbox>
|
||||
<mat-form-field style="margin-bottom: 42px;" color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="customOutput" [ngModelOptions]="{standalone: true}" [disabled]="!customOutputEnabled" matInput placeholder="Custom output" i18n-placeholder="Custom output placeholder">
|
||||
<input [(ngModel)]="customOutput" [ngModelOptions]="{standalone: true}" [disabled]="!customOutputEnabled" matInput (ngModelChange)="argsChanged()" placeholder="Custom output" i18n-placeholder="Custom 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 config download path. Don't include extension.</ng-container>
|
||||
@@ -140,13 +145,13 @@
|
||||
Use authentication
|
||||
</ng-container>
|
||||
</mat-checkbox>
|
||||
<mat-form-field color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Username" i18n-placeholder="YT Username placeholder">
|
||||
<mat-form-field *ngIf="youtubeAuthEnabled" color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()" placeholder="Username" i18n-placeholder="YT Username placeholder">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
|
||||
<mat-form-field style="margin-top: 31px;" color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Password" i18n-placeholder="YT Password placeholder">
|
||||
<mat-form-field *ngIf="youtubeAuthEnabled" style="margin-top: 31px;" color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()" placeholder="Password" i18n-placeholder="YT Password placeholder">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 mt-3">
|
||||
@@ -155,13 +160,13 @@
|
||||
Crop file
|
||||
</ng-container>
|
||||
</mat-checkbox>
|
||||
<mat-form-field color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="cropFileStart" type="number" [ngModelOptions]="{standalone: true}" [disabled]="!cropFile" matInput placeholder="Crop from (seconds)" i18n-placeholder="Crom from placeholder">
|
||||
<mat-form-field *ngIf="cropFile" color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="cropFileStart" type="number" [ngModelOptions]="{standalone: true}" matInput placeholder="Crop from (seconds)" i18n-placeholder="Crop from placeholder">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 mt-3">
|
||||
<mat-form-field style="margin-top: 31px;" color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="cropFileEnd" type="number" [ngModelOptions]="{standalone: true}" [disabled]="!cropFile" matInput placeholder="Crop to (seconds)" i18n-placeholder="Crop to placeholder">
|
||||
<mat-form-field *ngIf="cropFile" style="margin-top: 31px;" color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="cropFileEnd" type="number" [ngModelOptions]="{standalone: true}" matInput placeholder="Crop to (seconds)" i18n-placeholder="Crop to placeholder">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,20 +174,8 @@
|
||||
</mat-expansion-panel>
|
||||
</form>
|
||||
</div>
|
||||
<div *ngIf="multiDownloadMode && downloads.length > 0 && !current_download" style="margin-top: 15px;" class="big demo-basic">
|
||||
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;">
|
||||
<div class="container">
|
||||
<div *ngFor="let download of downloads; let i = index;" class="row">
|
||||
<ng-container *ngIf="current_download !== download && download['downloading']">
|
||||
<app-download-item style="width: 100%" [download]="download" [queueNumber]="i+1" (cancelDownload)="cancelDownload($event)"></app-download-item>
|
||||
<mat-divider style="position: relative" *ngIf="i !== downloads.length - 1"></mat-divider>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile">
|
||||
<div class="centered big" id="bar_div" *ngIf="current_download && autoplay">
|
||||
<div class="margined">
|
||||
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
|
||||
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
||||
@@ -197,9 +190,10 @@
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
<ng-template #nofile>
|
||||
|
||||
</ng-template>
|
||||
<div style="display: flex; justify-content: center;" *ngIf="downloads && downloads.length > 0 && !autoplay">
|
||||
<app-downloads style="width: 80%; margin-bottom: 10px" [uids]="download_uids"></app-downloads>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
||||
<app-recent-videos #recentVideos></app-recent-videos>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import {FileCardComponent} from '../file-card/file-card.component';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
@@ -9,7 +8,6 @@ import { saveAs } from 'file-saver';
|
||||
import { YoutubeSearchService, Result } from '../youtube-search.service';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
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';
|
||||
|
||||
@@ -46,10 +44,11 @@ export class MainComponent implements OnInit {
|
||||
determinateProgress = false;
|
||||
downloadingfile = false;
|
||||
audioOnly: boolean;
|
||||
multiDownloadMode = false;
|
||||
autoplay = false;
|
||||
customArgsEnabled = false;
|
||||
customArgs = null;
|
||||
customOutputEnabled = false;
|
||||
replaceArgs = false;
|
||||
customOutput = null;
|
||||
youtubeAuthEnabled = false;
|
||||
youtubeUsername = null;
|
||||
@@ -68,7 +67,7 @@ export class MainComponent implements OnInit {
|
||||
fileManagerEnabled = false;
|
||||
allowQualitySelect = false;
|
||||
downloadOnlyMode = false;
|
||||
allowMultiDownloadMode = false;
|
||||
allowAutoplay = false;
|
||||
audioFolderPath;
|
||||
videoFolderPath;
|
||||
use_youtubedl_archive = false;
|
||||
@@ -90,11 +89,11 @@ export class MainComponent implements OnInit {
|
||||
|
||||
mp3s: any[] = [];
|
||||
mp4s: any[] = [];
|
||||
files_cols = null;
|
||||
playlists = {'audio': [], 'video': []};
|
||||
playlist_thumbnails = {};
|
||||
downloading_content = {'audio': {}, 'video': {}};
|
||||
downloads: Download[] = [];
|
||||
download_uids: string[] = [];
|
||||
current_download: Download = null;
|
||||
|
||||
urlForm = new FormControl('', [Validators.required]);
|
||||
@@ -196,8 +195,6 @@ export class MainComponent implements OnInit {
|
||||
|
||||
@ViewChild('urlinput', { read: ElementRef }) urlInput: ElementRef;
|
||||
@ViewChild('recentVideos') recentVideos: RecentVideosComponent;
|
||||
@ViewChildren('audiofilecard') audioFileCards: QueryList<FileCardComponent>;
|
||||
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
|
||||
last_valid_url = '';
|
||||
last_url_check = 0;
|
||||
|
||||
@@ -211,6 +208,7 @@ export class MainComponent implements OnInit {
|
||||
error: false
|
||||
};
|
||||
|
||||
argsChangedSubject: Subject<boolean> = new Subject<boolean>();
|
||||
simulatedOutput = '';
|
||||
|
||||
constructor(public postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
|
||||
@@ -218,21 +216,19 @@ export class MainComponent implements OnInit {
|
||||
this.audioOnly = false;
|
||||
}
|
||||
|
||||
async configLoad() {
|
||||
async configLoad(): Promise<void> {
|
||||
await this.loadConfig();
|
||||
if (this.autoStartDownload) {
|
||||
this.downloadClicked();
|
||||
}
|
||||
|
||||
setInterval(() => this.getSimulatedOutput(), 1000);
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
async loadConfig(): Promise<boolean> {
|
||||
// loading config
|
||||
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled']
|
||||
&& (!this.postsService.isLoggedIn || this.postsService.permissions.includes('filemanager'));
|
||||
&& this.postsService.hasPermission('filemanager');
|
||||
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
|
||||
this.allowMultiDownloadMode = this.postsService.config['Extra']['allow_multi_download_mode'];
|
||||
this.allowAutoplay = this.postsService.config['Extra']['allow_autoplay'];
|
||||
this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
|
||||
this.videoFolderPath = this.postsService.config['Downloader']['path-video'];
|
||||
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
|
||||
@@ -242,15 +238,10 @@ export class MainComponent implements OnInit {
|
||||
this.youtubeAPIKey = this.youtubeSearchEnabled ? this.postsService.config['API']['youtube_API_key'] : null;
|
||||
this.allowQualitySelect = this.postsService.config['Extra']['allow_quality_select'];
|
||||
this.allowAdvancedDownload = this.postsService.config['Advanced']['allow_advanced_download']
|
||||
&& (!this.postsService.isLoggedIn || this.postsService.permissions.includes('advanced_download'));
|
||||
&& this.postsService.hasPermission('advanced_download');
|
||||
this.useDefaultDownloadingAgent = this.postsService.config['Advanced']['use_default_downloading_agent'];
|
||||
this.customDownloadingAgent = this.postsService.config['Advanced']['custom_downloading_agent'];
|
||||
|
||||
if (this.youtubeSearchEnabled && this.youtubeAPIKey) {
|
||||
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
|
||||
this.attachToInput();
|
||||
}
|
||||
|
||||
// set final cache items
|
||||
|
||||
localStorage.setItem('cached_filemanager_enabled', this.fileManagerEnabled.toString());
|
||||
@@ -265,6 +256,10 @@ export class MainComponent implements OnInit {
|
||||
this.customOutputEnabled = localStorage.getItem('customOutputEnabled') === 'true';
|
||||
}
|
||||
|
||||
if (localStorage.getItem('replaceArgs') !== null) {
|
||||
this.replaceArgs = localStorage.getItem('replaceArgs') === 'true';
|
||||
}
|
||||
|
||||
if (localStorage.getItem('youtubeAuthEnabled') !== null) {
|
||||
this.youtubeAuthEnabled = localStorage.getItem('youtubeAuthEnabled') === 'true';
|
||||
}
|
||||
@@ -274,9 +269,9 @@ export class MainComponent implements OnInit {
|
||||
const customOutput = localStorage.getItem('customOutput');
|
||||
const youtubeUsername = localStorage.getItem('youtubeUsername');
|
||||
|
||||
if (customArgs && customArgs !== 'null') { this.customArgs = customArgs };
|
||||
if (customOutput && customOutput !== 'null') { this.customOutput = customOutput };
|
||||
if (youtubeUsername && youtubeUsername !== 'null') { this.youtubeUsername = youtubeUsername };
|
||||
if (customArgs && customArgs !== 'null') { this.customArgs = customArgs }
|
||||
if (customOutput && customOutput !== 'null') { this.customOutput = customOutput }
|
||||
if (youtubeUsername && youtubeUsername !== 'null') { this.youtubeUsername = youtubeUsername }
|
||||
}
|
||||
|
||||
// get downloads routine
|
||||
@@ -290,7 +285,7 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
|
||||
// app initialization.
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
if (this.postsService.initialized) {
|
||||
this.configLoad();
|
||||
} else {
|
||||
@@ -314,8 +309,8 @@ export class MainComponent implements OnInit {
|
||||
this.audioOnly = localStorage.getItem('audioOnly') === 'true';
|
||||
}
|
||||
|
||||
if (localStorage.getItem('multiDownloadMode') !== null) {
|
||||
this.multiDownloadMode = localStorage.getItem('multiDownloadMode') === 'true';
|
||||
if (localStorage.getItem('autoplay') !== null) {
|
||||
this.autoplay = localStorage.getItem('autoplay') === 'true';
|
||||
}
|
||||
|
||||
// check if params exist
|
||||
@@ -327,57 +322,24 @@ export class MainComponent implements OnInit {
|
||||
this.autoStartDownload = true;
|
||||
}
|
||||
|
||||
this.setCols();
|
||||
this.argsChangedSubject
|
||||
.debounceTime(500)
|
||||
.subscribe((should_simulate) => {
|
||||
if (should_simulate) this.getSimulatedOutput();
|
||||
});
|
||||
}
|
||||
|
||||
public setCols() {
|
||||
if (window.innerWidth <= 350) {
|
||||
this.files_cols = 1;
|
||||
} else if (window.innerWidth <= 500) {
|
||||
this.files_cols = 2;
|
||||
} else if (window.innerWidth <= 750) {
|
||||
this.files_cols = 3
|
||||
} else {
|
||||
this.files_cols = 4;
|
||||
ngAfterViewInit(): void {
|
||||
if (this.youtubeSearchEnabled && this.youtubeAPIKey) {
|
||||
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
|
||||
this.attachToInput();
|
||||
}
|
||||
}
|
||||
|
||||
public goToFile(container, isAudio, uid) {
|
||||
this.downloadHelper(container, isAudio ? 'audio' : 'video', false, false, null, true);
|
||||
}
|
||||
|
||||
public goToPlaylist(playlistID, type) {
|
||||
const playlist = this.getPlaylistObjectByID(playlistID, type);
|
||||
if (playlist) {
|
||||
if (this.downloadOnlyMode) {
|
||||
this.downloading_content[type][playlistID] = true;
|
||||
this.downloadPlaylist(playlist);
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
const fileNames = playlist.fileNames;
|
||||
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]);
|
||||
}
|
||||
} else {
|
||||
// playlist not found
|
||||
console.error(`Playlist with ID ${playlistID} not found!`);
|
||||
}
|
||||
}
|
||||
|
||||
getPlaylistObjectByID(playlistID, type) {
|
||||
for (let i = 0; i < this.playlists[type].length; i++) {
|
||||
const playlist = this.playlists[type][i];
|
||||
if (playlist.id === playlistID) {
|
||||
return playlist;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// download helpers
|
||||
|
||||
downloadHelper(container, type, is_playlist = false, force_view = false, new_download = null, navigate_mode = false) {
|
||||
downloadHelper(container, type: string, is_playlist = false, force_view = false, navigate_mode = false): void {
|
||||
this.downloadingfile = false;
|
||||
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
||||
if (!this.autoplay && !this.downloadOnlyMode && !navigate_mode) {
|
||||
// do nothing
|
||||
this.reloadRecentVideos();
|
||||
} else {
|
||||
@@ -398,13 +360,10 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove download from current downloads
|
||||
this.removeDownloadFromCurrentDownloads(new_download);
|
||||
}
|
||||
|
||||
// download click handler
|
||||
downloadClicked() {
|
||||
downloadClicked(): void {
|
||||
if (!this.ValidURL(this.url)) {
|
||||
this.urlError = true;
|
||||
return;
|
||||
@@ -413,7 +372,8 @@ export class MainComponent implements OnInit {
|
||||
this.urlError = false;
|
||||
|
||||
// get common args
|
||||
const customArgs = (this.customArgsEnabled ? this.customArgs : null);
|
||||
const customArgs = (this.customArgsEnabled && this.replaceArgs ? this.customArgs : null);
|
||||
const additionalArgs = (this.customArgsEnabled && !this.replaceArgs ? this.customArgs : null);
|
||||
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
|
||||
const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null);
|
||||
const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null);
|
||||
@@ -432,21 +392,8 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
|
||||
const type = this.audioOnly ? 'audio' : 'video';
|
||||
// create download object
|
||||
const new_download: Download = {
|
||||
uid: uuid(),
|
||||
type: type,
|
||||
percent_complete: 0,
|
||||
url: this.url,
|
||||
downloading: true,
|
||||
is_playlist: this.url.includes('playlist'),
|
||||
error: false
|
||||
};
|
||||
this.downloads.push(new_download);
|
||||
if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
|
||||
this.downloadingfile = true;
|
||||
|
||||
let customQualityConfiguration = type === 'audio' ? this.getSelectedAudioFormat() : this.getSelectedVideoFormat();
|
||||
const customQualityConfiguration = type === 'audio' ? this.getSelectedAudioFormat() : this.getSelectedVideoFormat();
|
||||
|
||||
let cropFileSettings = null;
|
||||
|
||||
@@ -457,38 +404,31 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
this.postsService.downloadFile(this.url, type, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid, cropFileSettings).subscribe(res => {
|
||||
// update download object
|
||||
new_download.downloading = false;
|
||||
new_download.percent_complete = 100;
|
||||
const selected_quality = this.selectedQuality;
|
||||
this.selectedQuality = '';
|
||||
|
||||
const container = res['container'];
|
||||
const is_playlist = res['file_uids'].length > 1;
|
||||
|
||||
this.current_download = null;
|
||||
|
||||
this.downloadHelper(container, type, is_playlist, false, new_download);
|
||||
}, error => { // can't access server
|
||||
this.downloadingfile = true;
|
||||
this.postsService.downloadFile(this.url, type, (selected_quality === '' ? null : selected_quality),
|
||||
customQualityConfiguration, customArgs, additionalArgs, customOutput, youtubeUsername, youtubePassword, cropFileSettings).subscribe(res => {
|
||||
this.current_download = res['download'];
|
||||
this.downloads.push(res['download']);
|
||||
this.download_uids.push(res['download']['uid']);
|
||||
}, () => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.current_download = null;
|
||||
new_download['downloading'] = false;
|
||||
// removes download from list of downloads
|
||||
const downloads_index = this.downloads.indexOf(new_download);
|
||||
if (downloads_index !== -1) {
|
||||
this.downloads.splice(downloads_index)
|
||||
}
|
||||
this.openSnackBar('Download failed!', 'OK.');
|
||||
this.postsService.openSnackBar('Download failed!', 'OK.');
|
||||
});
|
||||
|
||||
if (this.multiDownloadMode) {
|
||||
if (!this.autoplay) {
|
||||
const download_queued_message = $localize`Download for ${this.url}:url: has been queued!`;
|
||||
this.postsService.openSnackBar(download_queued_message);
|
||||
this.url = '';
|
||||
this.downloadingfile = false;
|
||||
}
|
||||
}
|
||||
|
||||
// download canceled handler
|
||||
cancelDownload(download_to_cancel = null) {
|
||||
cancelDownload(download_to_cancel = null): void {
|
||||
// if one is provided, cancel that one. otherwise, remove the current one
|
||||
if (download_to_cancel) {
|
||||
this.removeDownloadFromCurrentDownloads(download_to_cancel)
|
||||
@@ -499,33 +439,32 @@ export class MainComponent implements OnInit {
|
||||
this.current_download = null;
|
||||
}
|
||||
|
||||
getSelectedAudioFormat() {
|
||||
getSelectedAudioFormat(): string {
|
||||
if (this.selectedQuality === '') { return null; }
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||
if (cachedFormatsExists) {
|
||||
const audio_formats = this.cachedAvailableFormats[this.url]['formats']['audio'];
|
||||
return this.selectedQuality['format_id'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedVideoFormat() {
|
||||
getSelectedVideoFormat(): string {
|
||||
if (this.selectedQuality === '') { return null; }
|
||||
const cachedFormats = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||
if (cachedFormats) {
|
||||
const video_formats = cachedFormats['video'];
|
||||
if (this.selectedQuality) {
|
||||
let selected_video_format = this.selectedQuality['format_id'];
|
||||
// add in audio format if necessary
|
||||
if (!this.selectedQuality['acodec'] && cachedFormats['best_audio_format']) selected_video_format += `+${cachedFormats['best_audio_format']}`;
|
||||
const audio_missing = !this.selectedQuality['acodec'] || this.selectedQuality['acodec'] === 'none';
|
||||
if (audio_missing && cachedFormats['best_audio_format']) selected_video_format += `+${cachedFormats['best_audio_format']}`;
|
||||
return selected_video_format;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getDownloadByUID(uid) {
|
||||
getDownloadByUID(uid: string) {
|
||||
const index = this.downloads.findIndex(download => download.uid === uid);
|
||||
if (index !== -1) {
|
||||
return this.downloads[index];
|
||||
@@ -534,7 +473,7 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
removeDownloadFromCurrentDownloads(download_to_remove) {
|
||||
removeDownloadFromCurrentDownloads(download_to_remove): boolean {
|
||||
if (this.current_download === download_to_remove) {
|
||||
this.current_download = null;
|
||||
}
|
||||
@@ -547,7 +486,7 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
downloadFileFromServer(file, type) {
|
||||
downloadFileFromServer(file, type: string): void {
|
||||
const ext = type === 'audio' ? 'mp3' : 'mp4'
|
||||
this.downloading_content[type][file.id] = true;
|
||||
this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
|
||||
@@ -557,13 +496,12 @@ export class MainComponent implements OnInit {
|
||||
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(file.uid).subscribe(delRes => {
|
||||
});
|
||||
this.postsService.deleteFile(file.uid).subscribe(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
downloadPlaylist(playlist) {
|
||||
downloadPlaylist(playlist): void {
|
||||
this.postsService.downloadPlaylistFromServer(playlist.id).subscribe(res => {
|
||||
if (playlist.id) { this.downloading_content[playlist.type][playlist.id] = false };
|
||||
const blob: Blob = res;
|
||||
@@ -572,25 +510,25 @@ export class MainComponent implements OnInit {
|
||||
|
||||
}
|
||||
|
||||
clearInput() {
|
||||
clearInput(): void {
|
||||
this.url = '';
|
||||
this.results_showing = false;
|
||||
}
|
||||
|
||||
onInputBlur() {
|
||||
onInputBlur(): void {
|
||||
this.results_showing = false;
|
||||
}
|
||||
|
||||
visitURL(url) {
|
||||
visitURL(url: string): void {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
useURL(url) {
|
||||
useURL(url: string): void {
|
||||
this.results_showing = false;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
inputChanged(new_val) {
|
||||
inputChanged(new_val: string): void {
|
||||
if (new_val === '' || !new_val) {
|
||||
this.results_showing = false;
|
||||
} else {
|
||||
@@ -601,7 +539,7 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
|
||||
// checks if url is a valid URL
|
||||
ValidURL(str) {
|
||||
ValidURL(str: string): boolean {
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
|
||||
const re = new RegExp(strRegex);
|
||||
@@ -617,20 +555,14 @@ export class MainComponent implements OnInit {
|
||||
if (str !== this.last_valid_url && this.allowQualitySelect) {
|
||||
// get info
|
||||
this.getURLInfo(str);
|
||||
this.argsChanged();
|
||||
}
|
||||
this.last_valid_url = str;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
// snackbar helper
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
getURLInfo(url) {
|
||||
getURLInfo(url: string): void {
|
||||
// if url is a youtube playlist, skip getting url info
|
||||
if (url.includes('playlist')) {
|
||||
return;
|
||||
@@ -640,7 +572,7 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
if (!(this.cachedAvailableFormats[url] && this.cachedAvailableFormats[url]['formats'])) {
|
||||
this.cachedAvailableFormats[url]['formats_loading'] = true;
|
||||
this.postsService.getFileInfo([url], 'irrelevant', true).subscribe(res => {
|
||||
this.postsService.getFileFormats([url]).subscribe(res => {
|
||||
this.cachedAvailableFormats[url]['formats_loading'] = false;
|
||||
const infos = res['result'];
|
||||
if (!infos || !infos.formats) {
|
||||
@@ -648,94 +580,54 @@ export class MainComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
this.cachedAvailableFormats[url]['formats'] = this.getAudioAndVideoFormats(infos.formats);
|
||||
console.log(this.cachedAvailableFormats[url]['formats']);
|
||||
}, err => {
|
||||
}, () => {
|
||||
this.errorFormats(url);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSimulatedOutput() {
|
||||
const customArgsExists = this.customArgsEnabled && this.customArgs;
|
||||
const globalArgsExists = this.globalCustomArgs && this.globalCustomArgs !== '';
|
||||
getSimulatedOutput(): void {
|
||||
// this function should be very similar to downloadClicked()
|
||||
const customArgs = (this.customArgsEnabled && this.replaceArgs ? this.customArgs : null);
|
||||
const additionalArgs = (this.customArgsEnabled && !this.replaceArgs ? this.customArgs : null);
|
||||
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
|
||||
const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null);
|
||||
const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null);
|
||||
|
||||
let full_string_array: string[] = [];
|
||||
const base_string_array = ['youtube-dl', this.url];
|
||||
const type = this.audioOnly ? 'audio' : 'video';
|
||||
|
||||
if (customArgsExists) {
|
||||
this.simulatedOutput = base_string_array.join(' ') + ' ' + this.customArgs.split(',,').join(' ');
|
||||
return this.simulatedOutput;
|
||||
}
|
||||
const customQualityConfiguration = type === 'audio' ? this.getSelectedAudioFormat() : this.getSelectedVideoFormat();
|
||||
|
||||
full_string_array.push(...base_string_array);
|
||||
let cropFileSettings = null;
|
||||
|
||||
const base_path = this.audioOnly ? this.audioFolderPath : this.videoFolderPath;
|
||||
const ext = this.audioOnly ? '.mp3' : '.mp4';
|
||||
// gets output
|
||||
let output_string_array = ['-o', base_path + '%(title)s' + ext];
|
||||
if (this.customOutputEnabled && this.customOutput) {
|
||||
output_string_array = ['-o', base_path + this.customOutput + ext];
|
||||
}
|
||||
// before pushing output, should check if using an external downloader
|
||||
if (!this.useDefaultDownloadingAgent && this.customDownloadingAgent === 'aria2c') {
|
||||
full_string_array.push('--external-downloader', 'aria2c');
|
||||
}
|
||||
// pushes output
|
||||
full_string_array.push(...output_string_array);
|
||||
|
||||
// logic splits into audio and video modes
|
||||
if (this.audioOnly) {
|
||||
// adds base audio string
|
||||
const format_array = [];
|
||||
const audio_format = this.getSelectedAudioFormat();
|
||||
if (audio_format) {
|
||||
format_array.push('-f', audio_format);
|
||||
} else if (this.selectedQuality) {
|
||||
format_array.push('--audio-quality', this.selectedQuality['format_id']);
|
||||
if (this.cropFile) {
|
||||
cropFileSettings = {
|
||||
cropFileStart: this.cropFileStart,
|
||||
cropFileEnd: this.cropFileEnd
|
||||
}
|
||||
|
||||
// pushes formats
|
||||
full_string_array.splice(2, 0, ...format_array);
|
||||
|
||||
const additional_params = ['-x', '--audio-format', 'mp3', '--write-info-json', '--print-json'];
|
||||
|
||||
full_string_array.push(...additional_params);
|
||||
} else {
|
||||
// adds base video string
|
||||
let format_array = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'];
|
||||
const video_format = this.getSelectedVideoFormat();
|
||||
if (video_format) {
|
||||
format_array = ['-f', video_format];
|
||||
} else if (this.selectedQuality) {
|
||||
format_array = [`bestvideo[height=${this.selectedQuality['format_id']}]+bestaudio/best[height=${this.selectedQuality}]`];
|
||||
}
|
||||
|
||||
// pushes formats
|
||||
full_string_array.splice(2, 0, ...format_array);
|
||||
|
||||
const additional_params = ['--write-info-json', '--print-json'];
|
||||
|
||||
full_string_array.push(...additional_params);
|
||||
}
|
||||
|
||||
if (this.use_youtubedl_archive) {
|
||||
full_string_array.push('--download-archive', 'archive.txt');
|
||||
}
|
||||
|
||||
if (globalArgsExists) {
|
||||
full_string_array = full_string_array.concat(this.globalCustomArgs.split(',,'));
|
||||
}
|
||||
|
||||
this.simulatedOutput = full_string_array.join(' ');
|
||||
return this.simulatedOutput;
|
||||
this.postsService.generateArgs(this.url, type, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration, customArgs, additionalArgs, customOutput, youtubeUsername, youtubePassword, cropFileSettings).subscribe(res => {
|
||||
const simulated_args = res['args'];
|
||||
if (simulated_args) {
|
||||
// hide password if needed
|
||||
const passwordIndex = simulated_args.indexOf('--password');
|
||||
console.log(passwordIndex);
|
||||
if (passwordIndex !== -1 && passwordIndex !== simulated_args.length - 1) {
|
||||
simulated_args[passwordIndex + 1] = simulated_args[passwordIndex + 1].replace(/./g, '*');
|
||||
}
|
||||
this.simulatedOutput = `youtube-dl ${this.url} ${simulated_args.join(' ')}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
errorFormats(url) {
|
||||
errorFormats(url: string): void {
|
||||
this.cachedAvailableFormats[url]['formats_loading'] = false;
|
||||
console.error('Could not load formats for url ' + url);
|
||||
}
|
||||
|
||||
attachToInput() {
|
||||
attachToInput(): void {
|
||||
Observable.fromEvent(this.urlInput.nativeElement, 'keyup')
|
||||
.map((e: any) => e.target.value) // extract the value of input
|
||||
.filter((text: string) => text.length > 1) // filter out if empty
|
||||
@@ -764,52 +656,44 @@ export class MainComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
onResize(event) {
|
||||
this.setCols();
|
||||
argsChanged(): void {
|
||||
this.argsChangedSubject.next(true);
|
||||
}
|
||||
|
||||
videoModeChanged(new_val) {
|
||||
videoModeChanged(new_val): void {
|
||||
this.selectedQuality = '';
|
||||
localStorage.setItem('audioOnly', new_val.checked.toString());
|
||||
this.argsChanged();
|
||||
}
|
||||
|
||||
multiDownloadModeChanged(new_val) {
|
||||
localStorage.setItem('multiDownloadMode', new_val.checked.toString());
|
||||
autoplayChanged(new_val): void {
|
||||
localStorage.setItem('autoplay', new_val.checked.toString());
|
||||
}
|
||||
|
||||
customArgsEnabledChanged(new_val) {
|
||||
customArgsEnabledChanged(new_val): void {
|
||||
localStorage.setItem('customArgsEnabled', new_val.checked.toString());
|
||||
if (new_val.checked === true && this.customOutputEnabled) {
|
||||
this.customOutputEnabled = false;
|
||||
localStorage.setItem('customOutputEnabled', 'false');
|
||||
|
||||
this.youtubeAuthEnabled = false;
|
||||
localStorage.setItem('youtubeAuthEnabled', 'false');
|
||||
}
|
||||
this.argsChanged();
|
||||
}
|
||||
|
||||
customOutputEnabledChanged(new_val) {
|
||||
replaceArgsChanged(new_val): void {
|
||||
localStorage.setItem('replaceArgs', new_val.checked.toString());
|
||||
this.argsChanged();
|
||||
}
|
||||
|
||||
customOutputEnabledChanged(new_val): void {
|
||||
localStorage.setItem('customOutputEnabled', new_val.checked.toString());
|
||||
if (new_val.checked === true && this.customArgsEnabled) {
|
||||
this.customArgsEnabled = false;
|
||||
localStorage.setItem('customArgsEnabled', 'false');
|
||||
}
|
||||
this.argsChanged();
|
||||
}
|
||||
|
||||
youtubeAuthEnabledChanged(new_val) {
|
||||
youtubeAuthEnabledChanged(new_val): void {
|
||||
localStorage.setItem('youtubeAuthEnabled', new_val.checked.toString());
|
||||
if (new_val.checked === true && this.customArgsEnabled) {
|
||||
this.customArgsEnabled = false;
|
||||
localStorage.setItem('customArgsEnabled', 'false');
|
||||
}
|
||||
this.argsChanged();
|
||||
}
|
||||
|
||||
getAudioAndVideoFormats(formats) {
|
||||
getAudioAndVideoFormats(formats): void {
|
||||
const audio_formats: any = {};
|
||||
const video_formats: any = {};
|
||||
|
||||
console.log(formats);
|
||||
|
||||
for (let i = 0; i < formats.length; i++) {
|
||||
const format_obj = {type: null};
|
||||
|
||||
@@ -865,7 +749,7 @@ export class MainComponent implements OnInit {
|
||||
return parsed_formats;
|
||||
}
|
||||
|
||||
getBestAudioFormatForMp4(audio_formats) {
|
||||
getBestAudioFormatForMp4(audio_formats): void {
|
||||
let best_audio_format_for_mp4 = null;
|
||||
let best_audio_format_bitrate = 0;
|
||||
const available_audio_format_keys = Object.keys(audio_formats);
|
||||
@@ -881,46 +765,8 @@ export class MainComponent implements OnInit {
|
||||
return best_audio_format_for_mp4;
|
||||
}
|
||||
|
||||
accordionEntered(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesMouseHovering = true;
|
||||
this.audioFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
} else if (type === 'video') {
|
||||
videoFilesMouseHovering = true;
|
||||
this.videoFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
accordionLeft(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesMouseHovering = false;
|
||||
} else if (type === 'video') {
|
||||
videoFilesMouseHovering = false;
|
||||
}
|
||||
}
|
||||
|
||||
accordionOpened(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesOpened = true;
|
||||
} else if (type === 'video') {
|
||||
videoFilesOpened = true;
|
||||
}
|
||||
}
|
||||
|
||||
accordionClosed(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesOpened = false;
|
||||
} else if (type === 'video') {
|
||||
videoFilesOpened = false;
|
||||
}
|
||||
}
|
||||
|
||||
// modify custom args
|
||||
openArgsModifierDialog() {
|
||||
openArgsModifierDialog(): void {
|
||||
const dialogRef = this.dialog.open(ArgModifierDialogComponent, {
|
||||
data: {
|
||||
initial_args: this.customArgs
|
||||
@@ -933,16 +779,24 @@ export class MainComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentDownload() {
|
||||
getCurrentDownload(): void {
|
||||
if (!this.current_download) {
|
||||
return;
|
||||
}
|
||||
const ui_uid = this.current_download['ui_uid'] ? this.current_download['ui_uid'] : this.current_download['uid'];
|
||||
this.postsService.getCurrentDownload(this.postsService.session_id, ui_uid).subscribe(res => {
|
||||
this.postsService.getCurrentDownload(this.current_download['uid']).subscribe(res => {
|
||||
if (res['download']) {
|
||||
if (ui_uid === res['download']['ui_uid']) {
|
||||
this.current_download = res['download'];
|
||||
this.percentDownloaded = this.current_download.percent_complete;
|
||||
this.current_download = res['download'];
|
||||
this.percentDownloaded = this.current_download.percent_complete;
|
||||
|
||||
if (this.current_download['finished'] && !this.current_download['error']) {
|
||||
const container = this.current_download['container'];
|
||||
const is_playlist = this.current_download['file_uids'].length > 1;
|
||||
this.downloadHelper(container, this.current_download['type'], is_playlist, false);
|
||||
this.current_download = null;
|
||||
} else if (this.current_download['finished'] && this.current_download['error']) {
|
||||
this.downloadingfile = false;
|
||||
this.current_download = null;
|
||||
this.postsService.openSnackBar('Download failed!', 'OK.');
|
||||
}
|
||||
} else {
|
||||
// console.log('failed to get new download');
|
||||
@@ -950,9 +804,7 @@ export class MainComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
reloadRecentVideos() {
|
||||
if (this.recentVideos) {
|
||||
this.recentVideos.getAllFiles();
|
||||
}
|
||||
reloadRecentVideos(): void {
|
||||
this.postsService.files_changed.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +89,10 @@
|
||||
display: inline-block;
|
||||
margin-right: 12px;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.skip-ad-button {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 75px;
|
||||
}
|
||||
@@ -4,8 +4,9 @@
|
||||
<mat-drawer-container style="height: 100%" class="example-container" autosize>
|
||||
<div style="height: fit-content" [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-col' : 'video-col'">
|
||||
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(currentItem.type === 'audio/mp3') ? postsService.theme.drawer_color : 'black'">
|
||||
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
|
||||
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls playsinline>
|
||||
</video>
|
||||
<app-skip-ad-button *ngIf="postsService['config']['API']['use_sponsorblock_API'] && api && playlist?.length > 0 && playlist[currentIndex]['type'] === 'video/mp4'" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [current_video]="playlist[currentIndex]" [playback_timestamp]="api.currentTime" [sponsor_block_cache]="sponsor_block_cache" class="skip-ad-button"></app-skip-ad-button>
|
||||
</vg-player>
|
||||
</div>
|
||||
<div style="height: fit-content; width: 100%; margin-top: 10px;">
|
||||
@@ -55,13 +56,13 @@
|
||||
</ng-container>
|
||||
</mat-drawer>
|
||||
|
||||
<div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
|
||||
<!-- <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> <mat-icon>update</mat-icon></button>
|
||||
|
||||
</div>
|
||||
</div> -->
|
||||
</mat-drawer-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit, ViewChild, ChangeDetectorRef } from '@angular/core';
|
||||
import { Component, OnInit, HostListener, OnDestroy, AfterViewInit, ViewChild, ChangeDetectorRef } from '@angular/core';
|
||||
import { VgApiService } from '@videogular/ngx-videogular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
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';
|
||||
@@ -15,6 +14,7 @@ export interface IMedia {
|
||||
src: string;
|
||||
type: string;
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -133,7 +133,8 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
title: this.name,
|
||||
label: this.name,
|
||||
src: this.url,
|
||||
type: 'video/mp4'
|
||||
type: 'video/mp4',
|
||||
url: this.url
|
||||
}
|
||||
this.playlist.push(imedia);
|
||||
this.currentItem = this.playlist[0];
|
||||
@@ -165,18 +166,8 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const subscription = res['subscription'];
|
||||
this.subscription = subscription;
|
||||
this.type === this.subscription.type;
|
||||
subscription.videos.forEach(video => {
|
||||
if (video['uid'] === this.uid) {
|
||||
this.db_file = video;
|
||||
this.postsService.incrementViewCount(this.db_file['uid'], this.sub_id, this.uuid).subscribe(res => {}, err => {
|
||||
console.error('Failed to increment view count');
|
||||
console.error(err);
|
||||
});
|
||||
this.uids = [this.db_file['uid']];
|
||||
this.show_player = true;
|
||||
this.parseFileNames();
|
||||
}
|
||||
});
|
||||
this.uids = this.subscription.videos.map(video => video['uid']);
|
||||
this.parseFileNames();
|
||||
}, err => {
|
||||
this.openSnackBar(`Failed to find subscription ${this.sub_id}`, 'Dismiss');
|
||||
});
|
||||
@@ -202,9 +193,14 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
parseFileNames() {
|
||||
this.playlist = [];
|
||||
for (let i = 0; i < this.uids.length; i++) {
|
||||
const uid = this.uids[i];
|
||||
|
||||
const file_obj = this.playlist_id ? this.db_playlist['file_objs'][i] : this.db_file;
|
||||
let file_obj = null;
|
||||
if (this.playlist_id) {
|
||||
file_obj = this.db_playlist['file_objs'][i];
|
||||
} else if (this.sub_id) {
|
||||
file_obj = this.subscription['videos'][i];
|
||||
} else {
|
||||
file_obj = this.db_file;
|
||||
}
|
||||
|
||||
const mime_type = file_obj.isAudio ? 'audio/mp3' : 'video/mp4'
|
||||
|
||||
@@ -229,10 +225,14 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
title: file_obj['title'],
|
||||
src: fullLocation,
|
||||
type: mime_type,
|
||||
label: file_obj['title']
|
||||
label: file_obj['title'],
|
||||
url: file_obj['url']
|
||||
}
|
||||
this.playlist.push(mediaObject);
|
||||
}
|
||||
if (this.db_playlist && this.db_playlist['randomize_order']) {
|
||||
this.shuffleArray(this.playlist);
|
||||
}
|
||||
this.currentItem = this.playlist[this.currentIndex];
|
||||
this.original_playlist = JSON.stringify(this.playlist);
|
||||
this.show_player = true;
|
||||
@@ -286,13 +286,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.currentItem = item;
|
||||
}
|
||||
|
||||
getFileInfos() {
|
||||
const fileNames = this.getFileNames();
|
||||
this.postsService.getFileInfo(fileNames, this.type, false).subscribe(res => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
getFileNames() {
|
||||
const fileNames = [];
|
||||
for (let i = 0; i < this.playlist.length; i++) {
|
||||
@@ -347,22 +340,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return JSON.stringify(this.playlist) !== this.original_playlist;
|
||||
}
|
||||
|
||||
updatePlaylist() {
|
||||
const fileNames = this.getFileNames();
|
||||
this.playlist_updating = true;
|
||||
this.postsService.updatePlaylistFiles(this.playlist_id, fileNames, this.type).subscribe(res => {
|
||||
this.playlist_updating = false;
|
||||
if (res['success']) {
|
||||
const fileNamesEncoded = fileNames.join('|nvr|');
|
||||
this.router.navigate(['/player', {fileNames: fileNamesEncoded, type: this.type, id: this.playlist_id}]);
|
||||
this.openSnackBar('Successfully updated playlist.', '');
|
||||
this.original_playlist = JSON.stringify(this.playlist);
|
||||
} else {
|
||||
this.openSnackBar('ERROR: Failed to update playlist.', '');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
openShareDialog() {
|
||||
const dialogRef = this.dialog.open(ShareMediaDialogComponent, {
|
||||
data: {
|
||||
@@ -409,6 +386,13 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.api.playbackRate = speed;
|
||||
}
|
||||
|
||||
shuffleArray(array) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
|
||||
// snackbar helper
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
|
||||
@@ -48,6 +48,9 @@ export class PostsService implements CanActivate {
|
||||
settings_changed = new BehaviorSubject<boolean>(false);
|
||||
open_create_default_admin_dialog = new BehaviorSubject<boolean>(false);
|
||||
|
||||
files_changed = new BehaviorSubject<boolean>(false);
|
||||
playlists_changed = new BehaviorSubject<boolean>(false);
|
||||
|
||||
// app status
|
||||
initialized = false;
|
||||
|
||||
@@ -57,6 +60,7 @@ export class PostsService implements CanActivate {
|
||||
categories = null;
|
||||
sidenav = null;
|
||||
locale = isoLangs['en'];
|
||||
version_info = null;
|
||||
|
||||
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
|
||||
public snackBar: MatSnackBar, private titleService: Title) {
|
||||
@@ -171,15 +175,28 @@ export class PostsService implements CanActivate {
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: max-line-length
|
||||
downloadFile(url: string, type: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null, cropFileSettings = null) {
|
||||
downloadFile(url: string, type: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, additionalArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, cropFileSettings = null) {
|
||||
return this.http.post(this.path + 'downloadFile', {url: url,
|
||||
selectedHeight: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration,
|
||||
customArgs: customArgs,
|
||||
additionalArgs: additionalArgs,
|
||||
customOutput: customOutput,
|
||||
youtubeUsername: youtubeUsername,
|
||||
youtubePassword: youtubePassword,
|
||||
type: type,
|
||||
cropFileSettings: cropFileSettings}, this.httpOptions);
|
||||
}
|
||||
|
||||
generateArgs(url: string, type: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, additionalArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, cropFileSettings = null) {
|
||||
return this.http.post(this.path + 'generateArgs', {url: url,
|
||||
selectedHeight: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration,
|
||||
customArgs: customArgs,
|
||||
additionalArgs: additionalArgs,
|
||||
customOutput: customOutput,
|
||||
youtubeUsername: youtubeUsername,
|
||||
youtubePassword: youtubePassword,
|
||||
ui_uid: ui_uid,
|
||||
type: type,
|
||||
cropFileSettings: cropFileSettings}, this.httpOptions);
|
||||
}
|
||||
@@ -192,8 +209,8 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'transferDB', {local_to_remote: local_to_remote}, this.httpOptions);
|
||||
}
|
||||
|
||||
testConnectionString() {
|
||||
return this.http.post(this.path + 'testConnectionString', {}, this.httpOptions);
|
||||
testConnectionString(connection_string) {
|
||||
return this.http.post(this.path + 'testConnectionString', {connection_string: connection_string}, this.httpOptions);
|
||||
}
|
||||
|
||||
killAllDownloads() {
|
||||
@@ -236,8 +253,8 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'getFile', {uid: uid, type: type, uuid: uuid}, this.httpOptions);
|
||||
}
|
||||
|
||||
getAllFiles() {
|
||||
return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions);
|
||||
getAllFiles(sort, range, text_search, file_type_filter) {
|
||||
return this.http.post(this.path + 'getAllFiles', {sort: sort, range: range, text_search: text_search, file_type_filter: file_type_filter}, this.httpOptions);
|
||||
}
|
||||
|
||||
getFullTwitchChat(id, type, uuid = null, sub = null) {
|
||||
@@ -293,8 +310,8 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'downloadArchive', {sub: sub}, {responseType: 'blob', params: this.httpOptions.params});
|
||||
}
|
||||
|
||||
getFileInfo(fileNames, type, urlMode) {
|
||||
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode}, this.httpOptions);
|
||||
getFileFormats(url) {
|
||||
return this.http.post(this.path + 'getFileFormats', {url: url}, this.httpOptions);
|
||||
}
|
||||
|
||||
getLogs(lines = 50) {
|
||||
@@ -334,18 +351,22 @@ export class PostsService implements CanActivate {
|
||||
include_file_metadata: include_file_metadata}, this.httpOptions);
|
||||
}
|
||||
|
||||
getPlaylists() {
|
||||
return this.http.post(this.path + 'getPlaylists', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
updatePlaylist(playlist) {
|
||||
return this.http.post(this.path + 'updatePlaylist', {playlist: playlist}, this.httpOptions);
|
||||
}
|
||||
|
||||
updatePlaylistFiles(playlistID, fileNames, type) {
|
||||
return this.http.post(this.path + 'updatePlaylistFiles', {playlistID: playlistID,
|
||||
fileNames: fileNames,
|
||||
type: type}, this.httpOptions);
|
||||
addFileToPlaylist(playlist_id, file_uid) {
|
||||
return this.http.post(this.path + 'addFileToPlaylist', {playlist_id: playlist_id,
|
||||
file_uid: file_uid},
|
||||
this.httpOptions);
|
||||
}
|
||||
|
||||
removePlaylist(playlistID, type) {
|
||||
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions);
|
||||
removePlaylist(playlist_id, type) {
|
||||
return this.http.post(this.path + 'deletePlaylist', {playlist_id: playlist_id, type: type}, this.httpOptions);
|
||||
}
|
||||
|
||||
// categories
|
||||
@@ -407,24 +428,50 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'getSubscriptions', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
// current downloads
|
||||
getCurrentDownloads() {
|
||||
return this.http.get(this.path + 'downloads', this.httpOptions);
|
||||
getCurrentDownloads(uids = null) {
|
||||
return this.http.post(this.path + 'downloads', {uids: uids}, this.httpOptions);
|
||||
}
|
||||
|
||||
// current download
|
||||
getCurrentDownload(session_id, download_id) {
|
||||
return this.http.post(this.path + 'download', {download_id: download_id, session_id: session_id}, this.httpOptions);
|
||||
getCurrentDownload(download_uid) {
|
||||
return this.http.post(this.path + 'download', {download_uid: download_uid}, this.httpOptions);
|
||||
}
|
||||
|
||||
// clear downloads. download_id is optional, if it exists only 1 download will be cleared
|
||||
clearDownloads(delete_all = false, session_id = null, download_id = null) {
|
||||
return this.http.post(this.path + 'clearDownloads', {delete_all: delete_all,
|
||||
download_id: download_id,
|
||||
session_id: session_id ? session_id : this.session_id}, this.httpOptions);
|
||||
pauseDownload(download_uid) {
|
||||
return this.http.post(this.path + 'pauseDownload', {download_uid: download_uid}, this.httpOptions);
|
||||
}
|
||||
|
||||
pauseAllDownloads() {
|
||||
return this.http.post(this.path + 'pauseAllDownloads', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
resumeDownload(download_uid) {
|
||||
return this.http.post(this.path + 'resumeDownload', {download_uid: download_uid}, this.httpOptions);
|
||||
}
|
||||
|
||||
resumeAllDownloads() {
|
||||
return this.http.post(this.path + 'resumeAllDownloads', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
restartDownload(download_uid) {
|
||||
return this.http.post(this.path + 'restartDownload', {download_uid: download_uid}, this.httpOptions);
|
||||
}
|
||||
|
||||
cancelDownload(download_uid) {
|
||||
return this.http.post(this.path + 'cancelDownload', {download_uid: download_uid}, this.httpOptions);
|
||||
}
|
||||
|
||||
clearDownload(download_uid) {
|
||||
return this.http.post(this.path + 'clearDownload', {download_uid: download_uid}, this.httpOptions);
|
||||
}
|
||||
|
||||
clearFinishedDownloads() {
|
||||
return this.http.post(this.path + 'clearFinishedDownloads', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
getVersionInfo() {
|
||||
return this.http.get(this.path + 'versionInfo', this.httpOptions);
|
||||
}
|
||||
|
||||
// updates the server to the latest version
|
||||
updateServer(tag) {
|
||||
return this.http.post(this.path + 'updateServer', {tag: tag}, this.httpOptions);
|
||||
}
|
||||
@@ -498,6 +545,12 @@ export class PostsService implements CanActivate {
|
||||
this.resetHttpParams();
|
||||
}
|
||||
|
||||
hasPermission(permission) {
|
||||
// assume not logged in users never have permission
|
||||
if (this.config.Advanced.multi_user_mode && !this.isLoggedIn) return false;
|
||||
return this.config.Advanced.multi_user_mode ? this.permissions.includes(permission) : true;
|
||||
}
|
||||
|
||||
// user methods
|
||||
register(username, password) {
|
||||
const call = this.http.post(this.path + 'auth/register', {userid: username,
|
||||
@@ -595,6 +648,11 @@ export class PostsService implements CanActivate {
|
||||
this.httpOptions);
|
||||
}
|
||||
|
||||
getSponsorBlockDataForVideo(id_hash) {
|
||||
const sponsor_block_api_path = 'https://sponsor.ajay.app/api/';
|
||||
return this.http.get(sponsor_block_api_path + `skipSegments/${id_hash}`);
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
|
||||
@@ -105,7 +105,7 @@ export const isoLangs = {
|
||||
'nativeName': 'ဗမာစာ'
|
||||
},
|
||||
'ca': {
|
||||
'name': 'Catalan; Valencian',
|
||||
'name': 'Catalan',
|
||||
'nativeName': 'Català'
|
||||
},
|
||||
'ch': {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
<h4 i18n="Settings title" mat-dialog-title>Settings</h4>
|
||||
<!-- <ng-container i18n="Allow subscriptions setting"></ng-container> -->
|
||||
<mat-dialog-content>
|
||||
|
||||
<!-- Language
|
||||
<div style="margin-bottom: 10px;">
|
||||
|
||||
</div> -->
|
||||
|
||||
<mat-tab-group>
|
||||
<h4 class="settings-title" i18n="Settings title">Settings</h4>
|
||||
<mat-tab-group style="height: 76vh" mat-align-tabs="center" [selectedIndex]="tabIndex" (selectedTabChange)="tabChanged($event)">
|
||||
<!-- Server -->
|
||||
<mat-tab label="Main" i18n-label="Main settings label">
|
||||
<ng-template matTabContent style="padding: 15px;">
|
||||
@@ -59,7 +51,7 @@
|
||||
<mat-hint><ng-container i18n="Check interval setting input hint">Unit is seconds, only include numbers.</ng-container></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-3">
|
||||
<div class="col-12 mt-4 mb-3">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Subscriptions']['redownload_fresh_uploads']" matTooltip="Sometimes new videos are downloaded before being fully processed. This setting will mean new videos will be checked for a higher quality version the following day." i18n-matTooltip="Redownload fresh uploads tooltip"><ng-container i18n="Redownload fresh uploads">Redownload fresh uploads</ng-container></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,14 +103,14 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-5">
|
||||
<div class="col-12 mt-3">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input matInput [(ngModel)]="new_config['Downloader']['path-video']" placeholder="Video folder path" i18n-placeholder="Video folder path input placeholder" required>
|
||||
<mat-hint><ng-container i18n="Video path setting input hint">Path for video downloads. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<div class="col-12 mt-3">
|
||||
<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">
|
||||
@@ -128,7 +120,7 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-5">
|
||||
<div class="col-12 mt-3 mb-4">
|
||||
<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>
|
||||
<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>
|
||||
@@ -142,7 +134,7 @@
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3">
|
||||
<h6 i18n="Categories">Categories</h6>
|
||||
<div cdkDropList class="category-list" (cdkDropListDropped)="dropCategory($event)">
|
||||
<div *ngIf="postsService.categories && postsService.categories.length > 0" kDropList 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']}}
|
||||
@@ -170,11 +162,32 @@
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_thumbnail']"><ng-container i18n="Include thumbnail setting">Include thumbnail</ng-container></mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
<div class="col-12 mt-2 mb-2">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_metadata']"><ng-container i18n="Include metadata setting">Include metadata</ng-container></mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3 mb-4">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input type="number" [(ngModel)]="new_config['Downloader']['max_concurrent_downloads']" matInput placeholder="Max concurrent downloads" i18n-placeholder="Max concurrent downloads">
|
||||
<mat-hint><ng-container i18n="Max concurrent downloads input hint">Limits the amount of downloads that can be simultaneously downloaded. Use -1 for no limit.</ng-container></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-4">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input [(ngModel)]="new_config['Downloader']['download_rate_limit']" matInput placeholder="Download rate limit" i18n-placeholder="Download rate limit input placeholder">
|
||||
<mat-hint><ng-container i18n="Download rate limit input hint">Rate limits your downloads to the specified amount. Ex: 200K</ng-container></mat-hint>
|
||||
</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">
|
||||
<button (click)="killAllDownloads()" mat-stroked-button color="warn"><ng-container i18n="Kill all downloads button">Kill all downloads</ng-container></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,7 +218,7 @@
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['download_only_mode']"><ng-container i18n="Download only mode setting">Download only mode</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['allow_multi_download_mode']"><ng-container i18n="Allow multi-download mode setting">Allow multi-download mode</ng-container></mat-checkbox>
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['allow_autoplay']"><ng-container i18n="Allow autoplay setting">Allow autoplay</ng-container></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,12 +259,18 @@
|
||||
<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">
|
||||
<div class="col-12">
|
||||
<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> <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 class="col-12 mt-4">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-3">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['generate_NFO_files']" matTooltip="Generates NFO files with every download, primarily used by Kodi." i18n-matTooltip="Generate NFO files tooltip"><ng-container i18n="Generate NFO files setting">Generate NFO files</ng-container></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
@@ -287,7 +306,6 @@
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3">
|
||||
<div *ngIf="db_info">
|
||||
<h5 i18n="Database info title">Database Info</h5>
|
||||
<p><ng-container i18n="Database location label">Database location:</ng-container> <strong>{{db_info['using_local_db'] ? 'Local' : 'MongoDB'}}</strong></p>
|
||||
<h6 i18n="Records per table label">Records per table</h6>
|
||||
<mat-list style="padding-top: 0px">
|
||||
@@ -298,16 +316,16 @@
|
||||
|
||||
<mat-form-field style="width: 100%; margin-top: 15px; margin-bottom: 10px" color="accent">
|
||||
<input [(ngModel)]="new_config['Database']['mongodb_connection_string']" matInput placeholder="MongoDB Connection String" i18n-placeholder="MongoDB Connection String" required>
|
||||
<mat-hint><ng-container i18n="MongoDB Connection String setting hint AKA preamble">Example:</ng-container> mongodb://127.0.0.1:27017/?compressors=zlib</mat-hint>
|
||||
<mat-hint><ng-container i18n="MongoDB Connection String setting hint AKA preamble">Example:</ng-container> mongodb://127.0.0.1:27017/?compressors=zlib<br>Docker: mongodb://<container name>:27017/?compressors=zlib</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<br>
|
||||
<div class="test-connection-div">
|
||||
<button (click)="testConnectionString(new_config['Database']['mongodb_connection_string'])" [disabled]="testing_connection_string" mat-flat-button color="accent"><ng-container i18n="Test connection string button">Test connection string</ng-container></button>
|
||||
</div>
|
||||
|
||||
<button (click)="testConnectionString()" [disabled]="testing_connection_string" mat-flat-button color="accent"><ng-container i18n="Test connection string button">Test connection string</ng-container></button>
|
||||
|
||||
<br>
|
||||
|
||||
<button class="transfer-db-button" [disabled]="db_transferring" color="accent" (click)="transferDB()" mat-raised-button><ng-container i18n="Transfer DB button">Transfer DB to </ng-container>{{db_info['using_local_db'] ? 'MongoDB' : 'Local'}}</button>
|
||||
<div class="transfer-db-div">
|
||||
<button [disabled]="db_transferring" color="accent" (click)="transferDB()" mat-raised-button><ng-container i18n="Transfer DB button">Transfer DB to </ng-container>{{db_info['using_local_db'] ? 'MongoDB' : 'Local'}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!db_info">
|
||||
<ng-container i18n="Database info not retrieved error message">Database information could not be retrieved. Check the server logs for more information.</ng-container>
|
||||
@@ -326,8 +344,9 @@
|
||||
<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-option value="youtube-dlc">youtube-dlc</mat-option>
|
||||
<mat-option value="yt-dlp">yt-dlp</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@@ -391,7 +410,7 @@
|
||||
<app-updater></app-updater>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div *ngIf="new_config" class="container">
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-4">
|
||||
<button (click)="restartServer()" mat-stroked-button color="warn"><ng-container i18n="Restart server button">Restart server</ng-container></button>
|
||||
@@ -400,71 +419,75 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
<mat-tab *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode" label="Users" i18n-label="Users settings label">
|
||||
|
||||
<div style="margin-left: 48px; margin-top: 24px; margin-bottom: -25px;">
|
||||
<div>
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
|
||||
<mat-tab [disabled]="!postsService.config?.Advanced.multi_user_mode">
|
||||
<ng-template mat-tab-label>
|
||||
<div [matTooltip]="!postsService.config?.Advanced.multi_user_mode ? usersTabDisabledTooltip : null">
|
||||
<ng-container i18n="Users settings label">Users</ng-container>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field style="margin-top: 15px;">
|
||||
<mat-select [(ngModel)]="new_config['Users']['auth_method']" placeholder="Auth method" i18n-placeholder="Auth method select">
|
||||
<mat-option value="internal">
|
||||
<ng-container i18n="Internal auth method">Internal</ng-container>
|
||||
</mat-option>
|
||||
<mat-option value="ldap">
|
||||
<ng-container i18n="LDAP auth method">LDAP</ng-container>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div *ngIf="new_config['Users']['auth_method'] === 'ldap'">
|
||||
</ng-template>
|
||||
<ng-container *ngIf="postsService.config?.Advanced.multi_user_mode">
|
||||
<div *ngIf="new_config" style="margin-top: 24px; margin-bottom: -25px;">
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="LDAP URL" placeholder="LDAP URL" [(ngModel)]="new_config['Users']['ldap_config']['url']">
|
||||
</mat-form-field>
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Bind DN" placeholder="Bind DN" [(ngModel)]="new_config['Users']['ldap_config']['bindDN']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Bind Credentials" placeholder="Bind Credentials" [(ngModel)]="new_config['Users']['ldap_config']['bindCredentials']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Search Base" placeholder="Search Base" [(ngModel)]="new_config['Users']['ldap_config']['searchBase']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Search Filter" placeholder="Search Filter" [(ngModel)]="new_config['Users']['ldap_config']['searchFilter']">
|
||||
</mat-form-field>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field style="margin-top: 15px;">
|
||||
<mat-select [(ngModel)]="new_config['Users']['auth_method']" placeholder="Auth method" i18n-placeholder="Auth method select">
|
||||
<mat-option value="internal">
|
||||
<ng-container i18n="Internal auth method">Internal</ng-container>
|
||||
</mat-option>
|
||||
<mat-option value="ldap">
|
||||
<ng-container i18n="LDAP auth method">LDAP</ng-container>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div *ngIf="new_config['Users']['auth_method'] === 'ldap'">
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="LDAP URL" placeholder="LDAP URL" [(ngModel)]="new_config['Users']['ldap_config']['url']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Bind DN" placeholder="Bind DN" [(ngModel)]="new_config['Users']['ldap_config']['bindDN']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Bind Credentials" placeholder="Bind Credentials" [(ngModel)]="new_config['Users']['ldap_config']['bindCredentials']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Search Base" placeholder="Search Base" [(ngModel)]="new_config['Users']['ldap_config']['searchBase']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Search Filter" placeholder="Search Filter" [(ngModel)]="new_config['Users']['ldap_config']['searchFilter']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
</div>
|
||||
<app-modify-users></app-modify-users>
|
||||
<app-modify-users *ngIf="new_config"></app-modify-users>
|
||||
</ng-container>
|
||||
</mat-tab>
|
||||
<mat-tab *ngIf="postsService.config" label="Logs" i18n-label="Logs settings label">
|
||||
<ng-template matTabContent>
|
||||
<div style="margin-left: 48px; margin-top: 24px; height: 340px">
|
||||
<div style="margin-top: 15px; height: 84%;">
|
||||
<app-logs-viewer></app-logs-viewer>
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<button color="accent" (click)="saveSettings()" [disabled]="settingsSame()" mat-raised-button><mat-icon>done</mat-icon>
|
||||
<ng-container i18n="Settings save button">Save</ng-container>
|
||||
</button>
|
||||
<button mat-flat-button [mat-dialog-close]="false"><mat-icon>cancel</mat-icon>
|
||||
<span i18n="Settings cancel and close button">{settingsAreTheSame + "", select, true {Close} false {Cancel} other {otha}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
<div class="action-buttons">
|
||||
<button style="margin-left: 10px; height: 37.3px" color="accent" (click)="saveSettings()" [disabled]="settingsSame()" mat-raised-button><mat-icon>done</mat-icon>
|
||||
<ng-container i18n="Settings save button">Save</ng-container>
|
||||
</button>
|
||||
<button style="margin-left: 10px;" mat-flat-button (click)="cancelSettings()" [disabled]="settingsSame()"><mat-icon>cancel</mat-icon>
|
||||
<span i18n="Settings cancel button">Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-tab-body {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.ext-divider {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@@ -23,7 +32,8 @@
|
||||
}
|
||||
|
||||
.text-field {
|
||||
min-width: 30%;
|
||||
width: 95%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.checkbox-button {
|
||||
@@ -83,7 +93,16 @@
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.transfer-db-button {
|
||||
margin-top: 10px;
|
||||
.test-connection-div {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.transfer-db-div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo
|
||||
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';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
@@ -20,7 +21,7 @@ import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/ed
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
all_locales = isoLangs;
|
||||
supported_locales = ['en', 'es', 'de', 'fr', 'nl', 'zh', 'nb', 'it', 'en-GB'];
|
||||
supported_locales = ['en', 'es', 'de', 'fr', 'nl', 'pt', 'it', 'ca', 'cs', 'nb', 'ru', 'zh', 'ko', 'id', 'en-GB'];
|
||||
initialLocale = localStorage.getItem('locale');
|
||||
|
||||
initial_config = null;
|
||||
@@ -38,25 +39,48 @@ export class SettingsComponent implements OnInit {
|
||||
latestGithubRelease = null;
|
||||
CURRENT_VERSION = CURRENT_VERSION
|
||||
|
||||
get settingsAreTheSame() {
|
||||
tabs = ['main', 'downloader', 'extra', 'database', 'advanced', 'users', 'logs'];
|
||||
tabIndex = 0;
|
||||
|
||||
INDEX_TO_TAB = Object.assign({}, this.tabs);
|
||||
TAB_TO_INDEX = {};
|
||||
|
||||
usersTabDisabledTooltip = $localize`You must enable multi-user mode to access this tab.`;
|
||||
|
||||
get settingsAreTheSame(): boolean {
|
||||
this._settingsSame = this.settingsSame()
|
||||
return this._settingsSame;
|
||||
}
|
||||
|
||||
set settingsAreTheSame(val) {
|
||||
set settingsAreTheSame(val: boolean) {
|
||||
this._settingsSame = val;
|
||||
}
|
||||
|
||||
constructor(public postsService: PostsService, private snackBar: MatSnackBar, private sanitizer: DomSanitizer,
|
||||
private dialog: MatDialog) { }
|
||||
private dialog: MatDialog, private router: Router, private route: ActivatedRoute) {
|
||||
// invert index to tab
|
||||
Object.keys(this.INDEX_TO_TAB).forEach(key => { this.TAB_TO_INDEX[this.INDEX_TO_TAB[key]] = key; });
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.getConfig();
|
||||
this.getDBInfo();
|
||||
if (this.postsService.initialized) {
|
||||
this.getConfig();
|
||||
this.getDBInfo();
|
||||
} else {
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
this.getConfig();
|
||||
this.getDBInfo();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.generated_bookmarklet_code = this.sanitizer.bypassSecurityTrustUrl(this.generateBookmarkletCode());
|
||||
|
||||
this.getLatestGithubRelease();
|
||||
|
||||
const tab = this.route.snapshot.paramMap.get('tab');
|
||||
this.tabIndex = tab && this.TAB_TO_INDEX[tab] ? this.TAB_TO_INDEX[tab] : 0;
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
@@ -85,6 +109,15 @@ export class SettingsComponent implements OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
cancelSettings() {
|
||||
this.new_config = JSON.parse(JSON.stringify(this.initial_config));
|
||||
}
|
||||
|
||||
tabChanged(event) {
|
||||
const index = event['index'];
|
||||
this.router.navigate(['/settings', {tab: this.INDEX_TO_TAB[index]}]);
|
||||
}
|
||||
|
||||
dropCategory(event: CdkDragDrop<string[]>) {
|
||||
moveItemInArray(this.postsService.categories, event.previousIndex, event.currentIndex);
|
||||
this.postsService.updateCategories(this.postsService.categories).subscribe(res => {
|
||||
@@ -307,9 +340,9 @@ export class SettingsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
testConnectionString() {
|
||||
testConnectionString(connection_string) {
|
||||
this.testing_connection_string = true;
|
||||
this.postsService.testConnectionString().subscribe(res => {
|
||||
this.postsService.testConnectionString(connection_string).subscribe(res => {
|
||||
this.testing_connection_string = false;
|
||||
if (res['success']) {
|
||||
this.postsService.openSnackBar('Connection successful!');
|
||||
|
||||
@@ -44,5 +44,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<button class="edit-button" color="primary" (click)="editSubscription()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">edit</mat-icon></button>
|
||||
<button class="watch-button" color="primary" (click)="watchSubscription()" mat-fab><mat-icon class="save-icon">video_library</mat-icon></button>
|
||||
<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>
|
||||
</div>
|
||||
@@ -67,4 +67,10 @@
|
||||
.save-icon {
|
||||
bottom: 1px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.watch-button {
|
||||
left: 90px;
|
||||
position: fixed;
|
||||
bottom: 25px;
|
||||
}
|
||||
@@ -109,8 +109,7 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
if (this.subscription.streamingOnly) {
|
||||
this.router.navigate(['/player', {uid: uid, url: url}]);
|
||||
} else {
|
||||
this.router.navigate(['/player', {uid: uid,
|
||||
sub_id: this.subscription.id}]);
|
||||
this.router.navigate(['/player', {uid: uid}]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,4 +170,8 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
watchSubscription() {
|
||||
this.router.navigate(['/player', {sub_id: this.subscription.id}])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
<ng-container i18n="Subscription playlist not available text">Name not available. Channel retrieval in progress.</ng-container>
|
||||
</div>
|
||||
</a>
|
||||
<button mat-icon-button (click)="editSubscription(sub)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="showSubInfo(sub)">
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SubscribeDialogComponent } from 'app/dialogs/subscribe-dialog/subscribe
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Router } from '@angular/router';
|
||||
import { SubscriptionInfoDialogComponent } from 'app/dialogs/subscription-info-dialog/subscription-info-dialog.component';
|
||||
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscriptions',
|
||||
@@ -32,8 +33,8 @@ export class SubscriptionsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
getSubscriptions() {
|
||||
this.subscriptions_loading = true;
|
||||
getSubscriptions(show_loading = true) {
|
||||
if (show_loading) this.subscriptions_loading = true;
|
||||
this.subscriptions = null;
|
||||
this.postsService.getAllSubscriptions().subscribe(res => {
|
||||
this.channel_subscriptions = [];
|
||||
@@ -102,6 +103,17 @@ export class SubscriptionsComponent implements OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
editSubscription(sub) {
|
||||
const dialogRef = this.dialog.open(EditSubscriptionDialogComponent, {
|
||||
data: {
|
||||
sub: this.postsService.getSubscriptionByID(sub.id)
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.getSubscriptions(false);
|
||||
});
|
||||
}
|
||||
|
||||
// snackbar helper
|
||||
public openSnackBar(message: string, action = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
|
||||
248
src/assets/i18n/messages.ca.json
Normal file
248
src/assets/i18n/messages.ca.json
Normal file
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Quant a",
|
||||
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Perfil",
|
||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "Fosc",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Paràmetres",
|
||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Inici",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Inicia la sessió",
|
||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Subscripcions",
|
||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Baixades",
|
||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Només àudio",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Baixa",
|
||||
"a38ae1082fec79ba1f379978337385a539a28e73": "Qualitat",
|
||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Utilitza un URL",
|
||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Visualitza",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Mode de baixades múltiples",
|
||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Cancel·la",
|
||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Avançat",
|
||||
"4e4c721129466be9c3862294dc40241b64045998": "Utilitza arguments personalitzats",
|
||||
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Arguments personalitzats",
|
||||
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "No cal incloure l’URL, només tot el que hi ha després. Separeu els arguments amb dues comes: ,,",
|
||||
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Utilitza una sortida personalitzada",
|
||||
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Sortida personalitzada",
|
||||
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentació",
|
||||
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "El camí és relatiu al camí de baixada de la configuració. No hi inclogueu l’extensió.",
|
||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Ordre simulada:",
|
||||
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Utilitza autenticació",
|
||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Nom d’usuari",
|
||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Contrasenya",
|
||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Crea una llista de reproducció",
|
||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Nom",
|
||||
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Tipus",
|
||||
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Àudio",
|
||||
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Vídeo",
|
||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Fitxers d’àudio",
|
||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Vídeos",
|
||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Subscriviu-vos a la llista o al canal",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
|
||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "L’URL de la llista o el canal",
|
||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nom personalitzat",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Baixa totes les pujades",
|
||||
"d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Qualitat màxima",
|
||||
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Mode només d’àudio",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Mode només de transmissió",
|
||||
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Aquestes s’afegeixen després dels arguments estàndards.",
|
||||
"98b6ec9ec138186d663e64770267b67334353d63": "Sortida de fitxer personalitzada",
|
||||
"d7b35c384aecd25a516200d6921836374613dfe7": "Cancel·la",
|
||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Subscriviu-vos",
|
||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Baixeu els vídeos penjats a la darrera",
|
||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Tipus:",
|
||||
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "Id.:",
|
||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Tanca",
|
||||
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Exporta l’arxiu",
|
||||
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Cancel·la la subscripció",
|
||||
"303e45ffae995c9817e510e38cb969e6bb3adcbf": "(En pausa)",
|
||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Arxiu:",
|
||||
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Nom:",
|
||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Carregador:",
|
||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Mida del fitxer:",
|
||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Camí:",
|
||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Data de pujada:",
|
||||
"0cc1dec590ecd74bef71a865fb364779bc42a749": "Categoria:",
|
||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modifica arguments del youtube-dl",
|
||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Arguments nous simulats",
|
||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "Afegeix un argument",
|
||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Cerca per categoria",
|
||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Utilitza el valor de l’argument",
|
||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Afegeix un argument",
|
||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modifica",
|
||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Valor de l’argument",
|
||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Actualitzador",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registra un usuari",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Nom d’usuari",
|
||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registra",
|
||||
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Puja galetes noves",
|
||||
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "NOTA: La càrrega de galetes noves anul·larà les galetes anteriors. Tingueu en compte també que les galetes són de tota la instància i no per usuari.",
|
||||
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrossegar i deixar anar",
|
||||
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modifica la llista de reproducció",
|
||||
"5caadefa4143cf6766a621b0f54f91f373a1f164": "Afegeix contingut",
|
||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Desa",
|
||||
"33026f57ea65cd9c8a5d917a08083f71a718933a": "Ordre normal",
|
||||
"29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Ordre invers",
|
||||
"d02888c485d3aeab6de628508f4a00312a722894": "Els meus vídeos",
|
||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Cerca",
|
||||
"73423607944a694ce6f9e55cfee329681bb4d9f9": "No s’ha trobat cap vídeo.",
|
||||
"3697f8583ea42868aa269489ad366103d94aece7": "Editant",
|
||||
"07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "Pausat",
|
||||
"c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Editant la categoria",
|
||||
"2489eefea00931942b91f4a1ae109514b591e2e1": "Regles",
|
||||
"e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Afegeix una regla nova",
|
||||
"792dc6a57f28a1066db283f2e736484f066005fd": "Baixa el xat del Twitch",
|
||||
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Edita",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Suprimeix",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Informació",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Recompte:",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Suprimeix i afegeix a la llista negra",
|
||||
"dad95154dcef3509b8cc705046061fd24994bbb7": "visualitzacions",
|
||||
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Desa els canvis",
|
||||
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "La descàrrega ha estat correcta",
|
||||
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "S'ha produït un error",
|
||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Detalls",
|
||||
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "S’ha produït un error:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Inici de la baixada:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Final de la baixada:",
|
||||
"ad127117f9471612f47d01eae09709da444a36a4": "Camins de fitxers:",
|
||||
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Les vostres subscripcions",
|
||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Canals",
|
||||
"47546e45bbb476baaaad38244db444c427ddc502": "Llistes de reproducció",
|
||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "El nom no està disponible. S’està recuperant el canal.",
|
||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "No tens cap subscripció al canal.",
|
||||
"2e0a410652cb07d069f576b61eab32586a18320d": "El nom no està disponible. Recuperant la llista de reproducció.",
|
||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "No tens cap subscripció a la llista de reproducció.",
|
||||
"82421c3e46a0453a70c42900eab51d58d79e6599": "Principal",
|
||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Descarregador",
|
||||
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avançat",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Usuaris",
|
||||
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registres",
|
||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Close} false {Cancel} other {otha}}",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL des d’on s’accedirà a aquesta aplicació, sense el port.",
|
||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Port",
|
||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "El port desitjat. Per defecte és 17442.",
|
||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "Mode multiusuari",
|
||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Ruta base dels usuaris",
|
||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "Ruta base per als usuaris i els seus vídeos descarregats.",
|
||||
"4e3120311801c4acd18de7146add2ee4a4417773": "Permet les subscripcions",
|
||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Ruta base de subscripcions",
|
||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "Ruta base per als vídeos dels vostres canals i llistes de reproducció subscrits. És relatiu a la carpeta arrel de YTDL-Material.",
|
||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Interval de comprovació",
|
||||
"0f56a7449b77630c114615395bbda4cab398efd8": "La unitat és de segons, només inclou números.",
|
||||
"13759b09a7f4074ceee8fa2f968f9815fdf63295": "De vegades, es descarreguen vídeos nous abans de processar-los completament. Aquesta configuració fa que, pels vídeos nous, es comprovi si hi ha una versió de més qualitat l'endemà.",
|
||||
"3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Torneu a descarregar les càrregues noves",
|
||||
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Tema",
|
||||
"ff7cee38a2259526c519f878e71b964f41db4348": "Per defecte",
|
||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Permet canviar el tema",
|
||||
"fe46ccaae902ce974e2441abe752399288298619": "Idioma",
|
||||
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Camí a la carpeta d’àudio",
|
||||
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Ruta de descàrregues de només d'àudio. És relatiu a la carpeta arrel de YTDL-Material.",
|
||||
"46826331da1949bd6fb74624447057099c9d20cd": "Ruta de la carpeta de vídeo",
|
||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Ruta de descàrregues de vídeo. És relatiu a la carpeta arrel de YTDL-Material.",
|
||||
"cfe829634b1144bc44b6d38cf5584ea65db9804f": "Sortida de fitxer per defecte",
|
||||
"1148fd45287ff09955b938756bc302042bcb29c7": "La ruta és relativa a les rutes de descàrrega anteriors. No inclogueu l'extensió.",
|
||||
"ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Arguments personalitzats globals",
|
||||
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Arguments personalitzats globals per a descàrregues a la pàgina inicial. Els arguments es delimiten amb dues comes, així: ,,",
|
||||
"04201f9d27abd7d6f58a4328ab98063ce1072006": "Categories",
|
||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Utilitzeu l'arxiu youtube-dl",
|
||||
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Inclou la miniatura",
|
||||
"384de8f8f112c9e6092eb2698706d391553f3e8d": "Inclou-hi metadades",
|
||||
"fb35145bfb84521e21b6385363d59221f436a573": "Mata totes les descàrregues",
|
||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Títol superior",
|
||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Gestor de fitxers habilitat",
|
||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Gestor de descàrregues habilitat",
|
||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Permet seleccionar la qualitat",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Mode de només baixades",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Permet el mode de descàrrega múltiple",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Habilita l'API pública",
|
||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Clau API pública",
|
||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Mostra la documentació",
|
||||
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Això suprimirà la vostra clau de l’API anterior!",
|
||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Genera",
|
||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Utilitza l’API del YouTube",
|
||||
"ce10d31febb3d9d60c160750570310f303a22c22": "Clau de l’API del YouTube",
|
||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "És senzill generar una clau!",
|
||||
"d162f9fcd6a7187b391e004f072ab3da8377c47d": "Utilitza l’API del Twitch",
|
||||
"8ae23bc4302a479f687f4b20a84c276182e2519c": "Clau de l’API del Twitch",
|
||||
"84ffcebac2709ca0785f4a1d5ba274433b5beabc": "També coneguda com a identificador de client.",
|
||||
"5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Baixa automàticament el xat del Twitch",
|
||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Feu clic aquí",
|
||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "per a baixar manualment l’extensió YouTubeDL-Material per al Chrome.",
|
||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Heu de carregar manualment l'extensió i modificar la seva configuració per configurar l'URL de la interfície.",
|
||||
"9a2ec6da48771128384887525bdcac992632c863": "per instal·lar l'extensió oficial de YoutubeDL-Material per a Firefox directament des de la pàgina d'extensions de Firefox.",
|
||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "Instruccions detallades de configuració.",
|
||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "No cal gaire cosa, a part de canviar la configuració de l'extensió per establir l'URL de la interfície.",
|
||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Arrossegueu l’enllaç següent als vostres adreces d'interès i ja podreu començar! Només cal que aneu al vídeo de YouTube que vulgueu baixar i feu clic al marcador.",
|
||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Genereu el marcador 'només àudio'",
|
||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Seleccioneu un baixador",
|
||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Utilitzeu l'agent de descàrrega predeterminat",
|
||||
"c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Seleccioneu un agent de descàrrega",
|
||||
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Nivell de registre",
|
||||
"db6c192032f4cab809aad35215f0aa4765761897": "Caducitat de l'inici de sessió",
|
||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Permet la descàrrega avançada",
|
||||
"431e5f3a0dde88768d1074baedd65266412b3f02": "Utilitza les galetes",
|
||||
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Defineix les galetes",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Permetre el registre d’usuari",
|
||||
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "Mètode d’autenticació",
|
||||
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Intern",
|
||||
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
|
||||
"1db9789b93069861019bd0ccaa5d4706b00afc61": "URL LDAP",
|
||||
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "DN de vinculació",
|
||||
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Credencials de vinculació",
|
||||
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Base de Cerca",
|
||||
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Filtre de cerca",
|
||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Quant al YoutubeDL-Material",
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "és un descarregador de YouTube de codi obert construït segons les especificacions de Material Design de Google. Podeu descarregar sense problemes els vostres vídeos preferits com a fitxers de vídeo o àudio i, fins i tot, subscriure-us als vostres canals i llistes de reproducció preferits per estar al dia amb els nous vídeos.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "té algunes funcions increïbles incloses. Una àmplia API, assistència de Docker i suport de localització (traducció). Feu clic a la icona de GitHub de més amunt per llegir totes les funcions compatibles.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versió instal·lada:",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "Heu trobat un error o teniu un suggeriment?",
|
||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "per crear un report de problema!",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "S’està comprovant si hi ha actualitzacions…",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Hi ha una actualització disponible",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Podeu actualitzar des del menú de paràmetres.",
|
||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "Seleccioneu una versió:",
|
||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Activa la compartició",
|
||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "Usa la marca de temps",
|
||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Segons",
|
||||
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Copia al porta-retalls",
|
||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Comparteix la llista",
|
||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Comparteix el vídeo",
|
||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Comparteix l’àudio",
|
||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Id. de la sessió:",
|
||||
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Neteja totes les baixades",
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(actual)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "No hi ha cap descàrrega disponible!",
|
||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "El teu perfil",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Tancar sessió",
|
||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Creat:",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "No heu iniciat la sessió.",
|
||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Crea un compte d'administrador",
|
||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "No s'ha detectat cap compte d'administrador predeterminat. Això crearà i definirà la contrasenya d'un compte d'administrador amb el nom d'usuari 'admin'.",
|
||||
"70a67e04629f6d412db0a12d51820b480788d795": "Crea",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Afegeix usuaris",
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Edita el rol",
|
||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Nom d’usuari",
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rol",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Accions",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Gestiona l'usuari",
|
||||
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Suprimeix l'usuari",
|
||||
"632e8b20c98e8eec4059a605a4b011bb476137af": "Edita l'usuari",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "UID d'usuari:",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nova contrasenya",
|
||||
"6498fa1b8f563988f769654a75411bb8060134b9": "Establir una contrasenya nova",
|
||||
"544e09cdc99a8978f48521d45f62db0da6dcf742": "Utilitzeu el rol per defecte",
|
||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Sí",
|
||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "No",
|
||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Gestiona el rol",
|
||||
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Línies:",
|
||||
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Esborra els registres",
|
||||
"24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "Generat automàticament",
|
||||
"ccf5ea825526ac490974336cb5c24352886abc07": "Obrir fitxer",
|
||||
"5656a06f17c24b2d7eae9c221567b209743829a9": "Obre el fitxer en una pestanya nova",
|
||||
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Anar a la subscripció",
|
||||
"94e01842dcee90531caa52e4147f70679bac87fe": "Suprimeix i torna a descarregar",
|
||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Esborra per sempre",
|
||||
"ddc31f2885b1b33a7651963254b0c197f2a64086": "Veure més.",
|
||||
"56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Veure menys.",
|
||||
"2054791b822475aeaea95c0119113de3200f5e1c": "Llargada:"
|
||||
}
|
||||
2517
src/assets/i18n/messages.ca.xlf
Normal file
2517
src/assets/i18n/messages.ca.xlf
Normal file
File diff suppressed because it is too large
Load Diff
248
src/assets/i18n/messages.cs.json
Normal file
248
src/assets/i18n/messages.cs.json
Normal file
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "O aplikaci",
|
||||
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Účet",
|
||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "Tmavý vzhled",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Nastavení",
|
||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Domů",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Přihlásit se",
|
||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Odběry",
|
||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Stažené",
|
||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Pouze Zvuk",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Stáhnout",
|
||||
"a38ae1082fec79ba1f379978337385a539a28e73": "Kvalita",
|
||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Použijte URL adresu",
|
||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Zobrazit",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Stahovat více zároveň",
|
||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Zrušit",
|
||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Pokročilé nastavení",
|
||||
"4e4c721129466be9c3862294dc40241b64045998": "Použít vlastní argumenty",
|
||||
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Vlastní argumenty",
|
||||
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Není třeba vkládat URL adresu, jen to co je za ní. Argumenty se oddělují pomocí dvou čárek za sebou takto: ,,",
|
||||
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Použít vlastní výstup",
|
||||
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Vlastní výstup",
|
||||
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Dokumentace",
|
||||
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "Cesta je relativní ke nakonfigurované cestě pro stahování. Nezahrnujte koncovky souborů.",
|
||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Simulovaný příkaz:",
|
||||
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Použit autentifikaci",
|
||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Uživatelské jméno",
|
||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Heslo",
|
||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Vytvořit playlist",
|
||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Název",
|
||||
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Typ",
|
||||
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio",
|
||||
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Video",
|
||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audio soubory",
|
||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Videa",
|
||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Odebírat playlist nebo kanál",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL adresa",
|
||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "URL adresa playlistu nebo kanálu",
|
||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Vlastní název",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Stáhnout všechny nahrávky",
|
||||
"d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Nejvyšší kvalita",
|
||||
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Stahovat pouze audio",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Režim pouze pro streamování",
|
||||
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Toto je přidáno za standartní argumenty.",
|
||||
"98b6ec9ec138186d663e64770267b67334353d63": "Vlastní výstup souboru",
|
||||
"d7b35c384aecd25a516200d6921836374613dfe7": "Zrušit odběr",
|
||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Odebírat",
|
||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Stahovat videa nahraná naposled v",
|
||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Typ:",
|
||||
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
|
||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Zavřít",
|
||||
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Exportovat Archiv",
|
||||
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Zrušit odběr",
|
||||
"303e45ffae995c9817e510e38cb969e6bb3adcbf": "(Pozastaveno)",
|
||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archiv:",
|
||||
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Název:",
|
||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Autor:",
|
||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Velikost souboru:",
|
||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Cesta:",
|
||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Datum Nahrání:",
|
||||
"0cc1dec590ecd74bef71a865fb364779bc42a749": "Kategorie:",
|
||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Upravid youtube-dl argumenty",
|
||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Nové simulované argumenty",
|
||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "Přidat argument",
|
||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Hledat podle kategorie",
|
||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Použít hodnotu argumentu",
|
||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Přidat argument",
|
||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Upravit",
|
||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Hodnota argumentu",
|
||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Aktualizace",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registrovat uživatele",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Uživatelské jméno",
|
||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrovat",
|
||||
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Nahrání nových cookies",
|
||||
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "POZNÁMKA: Nahráním nových cookies přepíšete vaše předchozí cookie. Vezměte na vědomí, že cookies jsou společné pro celou instanci, nikoliv rozdělené podle uživatelů.",
|
||||
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Přetáhněte sem",
|
||||
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Upravit playlist",
|
||||
"5caadefa4143cf6766a621b0f54f91f373a1f164": "Přidat obsah",
|
||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Uložit",
|
||||
"33026f57ea65cd9c8a5d917a08083f71a718933a": "Normální řazení",
|
||||
"29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Opačné řazení",
|
||||
"d02888c485d3aeab6de628508f4a00312a722894": "Moje videa",
|
||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Vyhledávání",
|
||||
"73423607944a694ce6f9e55cfee329681bb4d9f9": "Nebyla nalezena žádná videa.",
|
||||
"3697f8583ea42868aa269489ad366103d94aece7": "Úprava",
|
||||
"07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "Pozastaveno",
|
||||
"c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Úprava kategorie",
|
||||
"2489eefea00931942b91f4a1ae109514b591e2e1": "Pravidla",
|
||||
"e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Přidat nové pravidlo",
|
||||
"792dc6a57f28a1066db283f2e736484f066005fd": "Stáhnout Twitch Chat",
|
||||
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Upravit",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Odstranit",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Informace",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Počet:",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Odstranit a zablokovat",
|
||||
"dad95154dcef3509b8cc705046061fd24994bbb7": "zhlédnutí",
|
||||
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Uložit změny",
|
||||
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Stažení proběhlo úspěšně",
|
||||
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "Došlo k chybě",
|
||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Podrobnosti",
|
||||
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Došlo k chybě:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Zahájení stahování:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Konec stahování:",
|
||||
"ad127117f9471612f47d01eae09709da444a36a4": "Cesta k souboru:",
|
||||
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Vaše odběry",
|
||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanály",
|
||||
"47546e45bbb476baaaad38244db444c427ddc502": "Playlisty",
|
||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Název není dostupný. Probíhá vyhledávání kanálu.",
|
||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Nemáte žádné odběry.",
|
||||
"2e0a410652cb07d069f576b61eab32586a18320d": "Název není dostupný. Probíhá vyhledávání playlistu.",
|
||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Nemáte žádné odběry playlistů.",
|
||||
"82421c3e46a0453a70c42900eab51d58d79e6599": "Obecné",
|
||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Stahování",
|
||||
"d5f69691f9f05711633128b5a3db696783266b58": "Ostatní",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Pokročilé",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Uživatelé",
|
||||
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Záznamy",
|
||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Zavřít} false {Zrušit} other {Ostatní}}",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL adresa této aplikace, bez čísla portu.",
|
||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Číslo portu",
|
||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Momentálně nastavený port. Výchozí je 17442.",
|
||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "Režim více uživatelů",
|
||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Cesta pro uživatele",
|
||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "Základní cesta pro uživatele a jejich stažená videa.",
|
||||
"4e3120311801c4acd18de7146add2ee4a4417773": "Povolit odběry",
|
||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Základní cesta pro odběry",
|
||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "Základní cesta k videím z odebíraných kanálů a playlistů. Cesta je relativní k hlavní složce YTDL-Material.",
|
||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Interval kontroly",
|
||||
"0f56a7449b77630c114615395bbda4cab398efd8": "Hodnota ve vteřinách, použijte pouze číslice.",
|
||||
"13759b09a7f4074ceee8fa2f968f9815fdf63295": "Někdy jsou nová videa stažena dříve než mohou být plně zpracována. Toto nastavení znamená, že nová videa budou zkontrolována následující den pro zjištění, zda existují verze ve vyšší kvalitě.",
|
||||
"3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Znovu stáhnout čersvé nahrávky",
|
||||
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Vzhled",
|
||||
"ff7cee38a2259526c519f878e71b964f41db4348": "Výchozí",
|
||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Povolit změnu vzhledu",
|
||||
"fe46ccaae902ce974e2441abe752399288298619": "Jazyk",
|
||||
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Cesta pro složku s audio soubory",
|
||||
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Cesta pro složku se zvukovými soubory. Cesta je relativní k hlavní složce YTDL-Material.",
|
||||
"46826331da1949bd6fb74624447057099c9d20cd": "Cesta pro složku s video soubory",
|
||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Cesta pro složku s video soubory. Cesta je relativní k hlavní složce YTDL-Material.",
|
||||
"cfe829634b1144bc44b6d38cf5584ea65db9804f": "Výchozí výstup souboru",
|
||||
"1148fd45287ff09955b938756bc302042bcb29c7": "Cesta je relativní k cestám pro stažení výše. Nezahrnujte koncovky souborů.",
|
||||
"ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Globální vlastní argumenty",
|
||||
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Globální vlastní argumenty pro stahování na hlavní stránce. Argumenty se oddělují pomocí dvou čárek za sebou takto: ,,",
|
||||
"04201f9d27abd7d6f58a4328ab98063ce1072006": "Kategorie",
|
||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Použít archiv youtube-dl",
|
||||
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Zahrnout náhledový obrázek",
|
||||
"384de8f8f112c9e6092eb2698706d391553f3e8d": "Zahrnout metadata",
|
||||
"fb35145bfb84521e21b6385363d59221f436a573": "Zrušit všechna stahování",
|
||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Hlavní název",
|
||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Povolit správce souborů",
|
||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Povolit správce stahování",
|
||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Povolit výběr kvality",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Režim pouze stahování",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Povolit režim více stahování zároveň",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Povolit Veřejné API",
|
||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Veřejný API Klíč",
|
||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Zobrazit dokumentaci",
|
||||
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Tímto odstraníte svůj starý API klíč!",
|
||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Vygenerovat",
|
||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Použít YouTube API",
|
||||
"ce10d31febb3d9d60c160750570310f303a22c22": "YouTube API Klíč",
|
||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Vygenerovat klič je snadné!",
|
||||
"d162f9fcd6a7187b391e004f072ab3da8377c47d": "Použít Twitch API",
|
||||
"8ae23bc4302a479f687f4b20a84c276182e2519c": "Twitch API Klíč",
|
||||
"84ffcebac2709ca0785f4a1d5ba274433b5beabc": "Také známý jako ID Klienta.",
|
||||
"5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Automaticky stahovat Twitch Chat",
|
||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Klikněte zde",
|
||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "pro ruční stažení oficiálního Chrome doplňku YoutubeDL-Material.",
|
||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Musíte ručně nahrát rozšíření a upravit jeho nastavení tak, aby odkazovalo na URL adresu vaší domény.",
|
||||
"9a2ec6da48771128384887525bdcac992632c863": "pro instalaci oficiálního doplňku YouTubeDL-Material přímo z obchodu Firefox.",
|
||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "Podrobné instrukce k nastavení.",
|
||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Není toho potřeba mnoho, stačí v nastavení rozšíření změnit odkaz na URL adresu Vaší domény.",
|
||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Přetáhněte odkaz níže do svých záložek a je to! Poté si najděte na YouTube video, které byste chtěli stáhnout a klikněte na uloženou záložku.",
|
||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Vygenerovat záložku pro 'pouze zvuk'",
|
||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Vybrat službu pro stahování",
|
||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Použít výchozího agenta pro stahování",
|
||||
"c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Vybrat agenta pro stahování",
|
||||
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Úroveň Záznamů",
|
||||
"db6c192032f4cab809aad35215f0aa4765761897": "Vypršení přihlášení",
|
||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Povolit pokročilé nastavení stahování",
|
||||
"431e5f3a0dde88768d1074baedd65266412b3f02": "Použít Cookies",
|
||||
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Nastavit Cookies",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Povolit registraci uživatelů",
|
||||
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "Autentifikační metoda",
|
||||
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Interní",
|
||||
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
|
||||
"1db9789b93069861019bd0ccaa5d4706b00afc61": "URL adresa LDAP",
|
||||
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Bind-DN",
|
||||
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Přihlašovací údaje pro Bind",
|
||||
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Vyhledávací Základna",
|
||||
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Vyhledávací Filtr",
|
||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "O YoutubeDL-Material",
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "je open-source projekt pro stahování z YouTube postavený na specifikaci Material Designu od Google. Můžete pohodlně a bez problémů stahovat Vaše oblíbená videa jako video soubory nebo pouze zvukové soubory a dokonce odebírat Vaše oblíbené kanály a playlisty, aby Vám neunikla ta nejnovější videa.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "má spoustu skvělých funkcí! Rozšířitelné API, podporu Dockeru, překladů a lokalizace. Pro více informací o všech funkcích klikněte na GitHub ikonu výše.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Nainstalovaná verze:",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "Našli jste chybu nebo máte připomínku?",
|
||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "pro vytvoření Problému na GitHubu!",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Probíhá kontrola aktualizací...",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Dostupná aktualizace",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Můžete aktualizovat pomocí menu nastavení.",
|
||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "Vyberte verzi:",
|
||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Povolit sdílení",
|
||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "Použít časové razítko",
|
||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Vteřin",
|
||||
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Zkopírovat do schránky",
|
||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Sdílet playlist",
|
||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Sdílet video",
|
||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Sdílet audio",
|
||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID Relace:",
|
||||
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Vymazat všechna stahování",
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(momentální)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Nejsou dostupná žádná stahování!",
|
||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Váš Profil",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Odhlásit se",
|
||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Vytvořeno:",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Nejste přihlášen.",
|
||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Vytvořit administrátorský účet",
|
||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Nebyl zjištěn žádný administrátorský účet. Tímto se vytvoří administrátorský účet 'admin' a bude mu vytvořeno heslo.",
|
||||
"70a67e04629f6d412db0a12d51820b480788d795": "Vytvořit",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Přidat Uživatele",
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Upravit Roli",
|
||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Uživatelské jméno",
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Role",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Akce",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Spravovat uživatele",
|
||||
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Odstranit uživatele",
|
||||
"632e8b20c98e8eec4059a605a4b011bb476137af": "Upravit uživatele",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "UID Uživatele:",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nové heslo",
|
||||
"6498fa1b8f563988f769654a75411bb8060134b9": "Nastavit nové heslo",
|
||||
"544e09cdc99a8978f48521d45f62db0da6dcf742": "Použít výchozí nastavení role",
|
||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Ano",
|
||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Ne",
|
||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Spravovat roli",
|
||||
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Řádky:",
|
||||
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Vyčistit záznamy",
|
||||
"24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "Vytvořeno automaticky",
|
||||
"ccf5ea825526ac490974336cb5c24352886abc07": "Otevřít soubor",
|
||||
"5656a06f17c24b2d7eae9c221567b209743829a9": "Otevřít soubor na nové kartě",
|
||||
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Jít do odběrů",
|
||||
"94e01842dcee90531caa52e4147f70679bac87fe": "Odstranit a stáhnout znovu",
|
||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Odstranit navždy",
|
||||
"ddc31f2885b1b33a7651963254b0c197f2a64086": "Zobrazit více.",
|
||||
"56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Zobrazit méně.",
|
||||
"2054791b822475aeaea95c0119113de3200f5e1c": "Délka:"
|
||||
}
|
||||
2517
src/assets/i18n/messages.cs.xlf
Normal file
2517
src/assets/i18n/messages.cs.xlf
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,8 @@
|
||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "URL verwenden",
|
||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Ansehen",
|
||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Nur Audio",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Multi-Download Modus",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Download",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Multi-Herunteraden-Modus",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Herunterladen",
|
||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Abbrechen",
|
||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Erweitert",
|
||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Simulierter Befehl:",
|
||||
@@ -49,9 +49,9 @@
|
||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Schließen",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Anzahl:",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Info",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Infos",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Löschen",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Löschen und zur Blacklist hinzufügen",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Löschen und zur schwarzen Liste hinzufügen",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Einstellungen",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL, über die auf diese Applikation zugegriffen wird, ohne Port.",
|
||||
@@ -88,8 +88,8 @@
|
||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Dateimanager aktivieren",
|
||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Download-Manager aktivieren",
|
||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Qualitätsauswahl erlauben",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Nur Download Modus",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Multi-Download Modus erlauben",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Nur-Herunterladen-Modus",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Multi-Herunterladen-Modus erlauben",
|
||||
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Einstellungen durch PIN schützen",
|
||||
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Neuen PIN festlegen",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Öffentliche API aktivieren",
|
||||
@@ -120,7 +120,7 @@
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "ist ein quelloffener YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "beinhaltet viele tolle Funktionen! API, Docker und Lokalisierung werden unter anderem unterstützt. Informieren Sie sich über alle unterstützten Funktionen auf Github.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Installierte Version:",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Suche nach Updates ...",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Suche nach Aktualisierungen …",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Aktualisierung verfügbar",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Sie können über das Einstellungsmenü aktualisieren.",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "Haben Sie einen Fehler gefunden oder einen Vorschlag?",
|
||||
@@ -130,7 +130,7 @@
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Erstellt:",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Sie sind nicht angemeldet.",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Anmelden",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Ausloggen",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Abmelden",
|
||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Admin-Konto erstellen",
|
||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Es wurde kein Standard-Administratorkonto erkannt. Ein Administratorkonto mit dem Benutzernamen \"admin\" wird erstellt und ein Passwort wird festgelegt.",
|
||||
"70a67e04629f6d412db0a12d51820b480788d795": "Erstellen",
|
||||
@@ -149,16 +149,16 @@
|
||||
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Änderungen speichern",
|
||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Details",
|
||||
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Ein Fehler ist aufgetreten:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Download Start:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Download Ende:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Start des Herunterladen:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Ende des Herunterladen:",
|
||||
"ad127117f9471612f47d01eae09709da444a36a4": "Dateipfad(e):",
|
||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Wiedergabeliste oder einen Kanal",
|
||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "URL der Wiedergabeliste oder des Kanales",
|
||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Benutzerdefinierter Name",
|
||||
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Dies ist optional",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle Uploads herunterladen",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle hochgeladene Videos herunterladen",
|
||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Videos herunterladen aus den letzten",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Nur Streaming Modus",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Nur-Streaming-Modus",
|
||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Abonnieren",
|
||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Typ:",
|
||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archiv:",
|
||||
@@ -179,7 +179,7 @@
|
||||
"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",
|
||||
@@ -199,7 +199,7 @@
|
||||
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Logs erscheinen hier",
|
||||
"98b6ec9ec138186d663e64770267b67334353d63": "Benutzerdefinierte Dateiausgabe",
|
||||
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Diese werden nach den Standardargumenten hinzugefügt.",
|
||||
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Nur-Audio Modus",
|
||||
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Nur-Audio-Modus",
|
||||
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Protokolle",
|
||||
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Cookies setzen",
|
||||
"431e5f3a0dde88768d1074baedd65266412b3f02": "Cookies verwenden",
|
||||
@@ -221,5 +221,50 @@
|
||||
"511b600ae4cf037e4eb3b7a58410842cd5727490": "Weitere Inhalte hinzufügen",
|
||||
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Typ",
|
||||
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Video",
|
||||
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio"
|
||||
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio",
|
||||
"d02888c485d3aeab6de628508f4a00312a722894": "Meine Videos",
|
||||
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Zu den Abonnements gehen",
|
||||
"5656a06f17c24b2d7eae9c221567b209743829a9": "Datei in einer neuen Registerkarte öffnen",
|
||||
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Herunterladen-Liste leeren",
|
||||
"544e09cdc99a8978f48521d45f62db0da6dcf742": "Rollenstandard verwenden",
|
||||
"ccf5ea825526ac490974336cb5c24352886abc07": "Datei öffnen",
|
||||
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Protokolldatei löschen",
|
||||
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Benutzer löschen",
|
||||
"632e8b20c98e8eec4059a605a4b011bb476137af": "Benutzer bearbeiten",
|
||||
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "Authentifizierungsmethode",
|
||||
"3697f8583ea42868aa269489ad366103d94aece7": "Bearbeiten",
|
||||
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "Ein Fehler ist aufgetreten",
|
||||
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Der Download war erfolgreich",
|
||||
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Search Base",
|
||||
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Bind Credentials",
|
||||
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Bind DN",
|
||||
"1db9789b93069861019bd0ccaa5d4706b00afc61": "LDAP URL",
|
||||
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
|
||||
"5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Twitch-Chat automatisch herunterladen",
|
||||
"84ffcebac2709ca0785f4a1d5ba274433b5beabc": "Wird auch als Client-ID bezeichnet.",
|
||||
"8ae23bc4302a479f687f4b20a84c276182e2519c": "Twitch-API-Schlüssel",
|
||||
"d162f9fcd6a7187b391e004f072ab3da8377c47d": "Twitch-API verwenden",
|
||||
"04201f9d27abd7d6f58a4328ab98063ce1072006": "Kategorien",
|
||||
"3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Neue Uploads erneut herunterladen",
|
||||
"792dc6a57f28a1066db283f2e736484f066005fd": "Twitch-Chat herunterladen",
|
||||
"e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Neue Regel hinzufügen",
|
||||
"2489eefea00931942b91f4a1ae109514b591e2e1": "Regeln",
|
||||
"c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Kategorie bearbeiten",
|
||||
"07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "Pausiert",
|
||||
"73423607944a694ce6f9e55cfee329681bb4d9f9": "Keine Videos gefunden.",
|
||||
"29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Umgekehrte Reihenfolge",
|
||||
"33026f57ea65cd9c8a5d917a08083f71a718933a": "Normale Reihenfolge",
|
||||
"5caadefa4143cf6766a621b0f54f91f373a1f164": "Inhalt hinzufügen",
|
||||
"0cc1dec590ecd74bef71a865fb364779bc42a749": "Kategorie:",
|
||||
"303e45ffae995c9817e510e38cb969e6bb3adcbf": "(Pausiert)",
|
||||
"56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Weniger sehen.",
|
||||
"ddc31f2885b1b33a7651963254b0c197f2a64086": "Mehr sehen.",
|
||||
"24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "Automatisch generiert",
|
||||
"c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Wähle einen Download-Agenten",
|
||||
"ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Globale benutzerdefinierte Argumente",
|
||||
"1148fd45287ff09955b938756bc302042bcb29c7": "Der Pfad ist relativ zu den darüberliegenden Downloadpfaden. Erweiterung auslassen.",
|
||||
"cfe829634b1144bc44b6d38cf5584ea65db9804f": "Standard-Dateiausgabe",
|
||||
"13759b09a7f4074ceee8fa2f968f9815fdf63295": "Manchmal werden neue Videos heruntergeladen bevor diese von der Videoplattform vollständig verarbeitet wurden. Diese Einstellung bewirkt, dass am Folgetag neue Videos auf eine höhere Auflösung überprüft werden.",
|
||||
"dad95154dcef3509b8cc705046061fd24994bbb7": "Aufrufe",
|
||||
"d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Maximale Qualität"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -30,8 +30,8 @@
|
||||
"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",
|
||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Nom d'utilisateur",
|
||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Mot de passe",
|
||||
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
|
||||
"9779715ac05308973d8f1c8658b29b986e92450f": "Vos fichiers audio sont ici",
|
||||
"47546e45bbb476baaaad38244db444c427ddc502": "Listes de lecture",
|
||||
@@ -41,7 +41,7 @@
|
||||
"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 :",
|
||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Chaîne :",
|
||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Taille du fichier :",
|
||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Chemin :",
|
||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Mise en ligne :",
|
||||
@@ -49,7 +49,7 @@
|
||||
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modifier la liste de lecture",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID :",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Compteur :",
|
||||
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editer",
|
||||
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Modifier",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Effacer",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Informations",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Supprimer et bannir",
|
||||
@@ -77,7 +77,7 @@
|
||||
"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",
|
||||
"ff7cee38a2259526c519f878e71b964f41db4348": "Par défaut",
|
||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "Sombre",
|
||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Autoriser le changement du thème",
|
||||
"fe46ccaae902ce974e2441abe752399288298619": "Choix de la langue",
|
||||
@@ -117,8 +117,8 @@
|
||||
"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",
|
||||
"431e5f3a0dde88768d1074baedd65266412b3f02": "Utiliser les cookies",
|
||||
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Gérer les cookies",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avancé",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Autoriser l'enregistrement des utilisateurs",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Utilisateurs",
|
||||
@@ -138,8 +138,8 @@
|
||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID :",
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Créé le :",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Vous n'êtes pas identifié.",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Identifiant",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Déconnexion",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Se connecter",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Se déconnecter",
|
||||
"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",
|
||||
@@ -149,8 +149,8 @@
|
||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements",
|
||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Téléchargements",
|
||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Partager une liste de lecture",
|
||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Partager vidéo",
|
||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Partager audio",
|
||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Partager une vidéo",
|
||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Partager un audio",
|
||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Activer le partage",
|
||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "Utiliser l'horodatage",
|
||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Secondes",
|
||||
@@ -192,7 +192,7 @@
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(actual)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Pas de téléchargements !",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Ajouter un utilisateur",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Identifiant",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Nom d'utilisateur",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Gérer l'utilisateur",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Identifiant de l'utilisateur :",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nouveau mot de passe",
|
||||
@@ -224,5 +224,46 @@
|
||||
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Type",
|
||||
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Vidéo",
|
||||
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio",
|
||||
"3697f8583ea42868aa269489ad366103d94aece7": "Édition"
|
||||
"3697f8583ea42868aa269489ad366103d94aece7": "Édition",
|
||||
"d02888c485d3aeab6de628508f4a00312a722894": "Mes vidéos",
|
||||
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Aller aux abonnements",
|
||||
"5656a06f17c24b2d7eae9c221567b209743829a9": "Ouvrir le fichier dans un nouvel onglet",
|
||||
"ccf5ea825526ac490974336cb5c24352886abc07": "Ouvrir le fichier",
|
||||
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Effacer les journaux",
|
||||
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Supprimer l'utilisateur",
|
||||
"544e09cdc99a8978f48521d45f62db0da6dcf742": "Utiliser le groupe par défaut",
|
||||
"632e8b20c98e8eec4059a605a4b011bb476137af": "Modifier l'utilisateur",
|
||||
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Effacer tous les téléchargements",
|
||||
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Search Base",
|
||||
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Bind Credentials",
|
||||
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Bind DN",
|
||||
"1db9789b93069861019bd0ccaa5d4706b00afc61": "LDAP URL",
|
||||
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
|
||||
"56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Voir moins.",
|
||||
"ddc31f2885b1b33a7651963254b0c197f2a64086": "Voir plus.",
|
||||
"24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "Généré automatiquement",
|
||||
"c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Sélectionnez un agent de téléchargement",
|
||||
"5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Télécharger automatiquement la discussion Twitch",
|
||||
"84ffcebac2709ca0785f4a1d5ba274433b5beabc": "Aussi connu sous le nom d'identifiant client.",
|
||||
"8ae23bc4302a479f687f4b20a84c276182e2519c": "Clé API Twitch",
|
||||
"d162f9fcd6a7187b391e004f072ab3da8377c47d": "Utiliser l'API de Twitch",
|
||||
"04201f9d27abd7d6f58a4328ab98063ce1072006": "Catégories",
|
||||
"ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Arguments personnalisés globaux",
|
||||
"1148fd45287ff09955b938756bc302042bcb29c7": "Le chemin est relatif aux chemins de téléchargement ci-dessus. Ne pas inclure l'extension.",
|
||||
"cfe829634b1144bc44b6d38cf5584ea65db9804f": "Sortie de fichier par défaut",
|
||||
"3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Retélécharger les nouvelles mises en ligne",
|
||||
"13759b09a7f4074ceee8fa2f968f9815fdf63295": "Il arrive que de nouvelles vidéos soient téléchargées avant d'être entièrement traitées. Grâce à ce paramètre, les nouvelles vidéos seront vérifiées pour obtenir une version de meilleure qualité le jour suivant.",
|
||||
"dad95154dcef3509b8cc705046061fd24994bbb7": "vues",
|
||||
"792dc6a57f28a1066db283f2e736484f066005fd": "Télécharger la discussion Twitch",
|
||||
"e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Ajouter une nouvelle règle",
|
||||
"2489eefea00931942b91f4a1ae109514b591e2e1": "Règles",
|
||||
"c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Modification de la catégorie",
|
||||
"07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "En pause",
|
||||
"73423607944a694ce6f9e55cfee329681bb4d9f9": "Aucune vidéo trouvée.",
|
||||
"29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Ordre inverse",
|
||||
"33026f57ea65cd9c8a5d917a08083f71a718933a": "Ordre normal",
|
||||
"5caadefa4143cf6766a621b0f54f91f373a1f164": "Ajouter du contenu",
|
||||
"0cc1dec590ecd74bef71a865fb364779bc42a749": "Catégorie :",
|
||||
"303e45ffae995c9817e510e38cb969e6bb3adcbf": "(En pause)",
|
||||
"d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Qualité maximale"
|
||||
}
|
||||
2578
src/assets/i18n/messages.fr.xlf
Normal file
2578
src/assets/i18n/messages.fr.xlf
Normal file
File diff suppressed because it is too large
Load Diff
248
src/assets/i18n/messages.id.json
Normal file
248
src/assets/i18n/messages.id.json
Normal file
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Tentang",
|
||||
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profil",
|
||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "Gelap",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Pengaturan",
|
||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Beranda",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Masuk",
|
||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Langganan",
|
||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Unduhan",
|
||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Hanya Audio",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Unduhan",
|
||||
"a38ae1082fec79ba1f379978337385a539a28e73": "Kualitas",
|
||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Gunakan Alamat URL",
|
||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Tampilkan",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Mode Multi-unduh",
|
||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Batal",
|
||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Khusus",
|
||||
"4e4c721129466be9c3862294dc40241b64045998": "Gunakan arg kustom",
|
||||
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Arg kustom",
|
||||
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Tidak perlu mencantumkan alamat URL, hanya bagian setelahnya saja. Arg dibatasi menggunakan dua koma seperti ini: ,,",
|
||||
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Gunakan output kustom",
|
||||
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Output kustom",
|
||||
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Dokumentasi",
|
||||
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "Path adalah relatif kepada path unduh konfigurasi. Jangan mencantumkan ektensi.",
|
||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Perintah yang disimulasi:",
|
||||
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Gunakan otentikasi",
|
||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Nama Pengguna",
|
||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Kata Sandi",
|
||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Buat sebuah playlist",
|
||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Nama",
|
||||
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Jenis",
|
||||
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio",
|
||||
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Video",
|
||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Berkas audio",
|
||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Video",
|
||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Langganan ke playlist atau saluran/channel",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "Alamat URL",
|
||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "Playlist atau alamat URL saluran/channel",
|
||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nama kustom",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Unduh semua unggahan",
|
||||
"d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Kualitas maksimal",
|
||||
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Mode hanya-audio",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Mode hanya-streaming",
|
||||
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Ini ditambahkan setelah argumen standar.",
|
||||
"98b6ec9ec138186d663e64770267b67334353d63": "Output berkas kustom",
|
||||
"d7b35c384aecd25a516200d6921836374613dfe7": "Batal",
|
||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Langganan",
|
||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Unduh video yang diunggah terakhir",
|
||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Jenis:",
|
||||
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "Alamat URL:",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
|
||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Tutup",
|
||||
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Ekspor Arsip",
|
||||
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Batal Langganan",
|
||||
"303e45ffae995c9817e510e38cb969e6bb3adcbf": "(Dijeda)",
|
||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Arsip:",
|
||||
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Nama:",
|
||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Pengunggah:",
|
||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Ukuran berkas:",
|
||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Jalur/path:",
|
||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Tanggal Unggah:",
|
||||
"0cc1dec590ecd74bef71a865fb364779bc42a749": "Kategori:",
|
||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modifikasi arg youtube-dl",
|
||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Simulasikan arg baru",
|
||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "Tambah sebuah arg",
|
||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Cari berdasarkan kategori",
|
||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Gunakan nilai arg",
|
||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Tambah arg",
|
||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modifikasi",
|
||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Nilai arg",
|
||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Pembaruan",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Daftarkan pengguna",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Nama pengguna",
|
||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Daftar",
|
||||
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Unggah cookie baru",
|
||||
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "CATATAN: Mengunggah cookie baru akan menimpa cookie Anda sebelumnya. Juga ingat bahwa cookie termasuk dalam pengguna-banyak, bukan per-pengguna.",
|
||||
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Tarik dan Jatuhkan",
|
||||
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modifikasi playlist",
|
||||
"5caadefa4143cf6766a621b0f54f91f373a1f164": "Tambah konten",
|
||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Simpan",
|
||||
"33026f57ea65cd9c8a5d917a08083f71a718933a": "Urutan normal",
|
||||
"29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Urutan terbalik",
|
||||
"d02888c485d3aeab6de628508f4a00312a722894": "Video saya",
|
||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Cari",
|
||||
"73423607944a694ce6f9e55cfee329681bb4d9f9": "Tidak ada video ditemukan.",
|
||||
"3697f8583ea42868aa269489ad366103d94aece7": "Menyunting",
|
||||
"07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "Dijeda",
|
||||
"c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Kategori suntingan",
|
||||
"2489eefea00931942b91f4a1ae109514b591e2e1": "Aturan",
|
||||
"e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Tambah aturan baru",
|
||||
"792dc6a57f28a1066db283f2e736484f066005fd": "Unduh Twitch Chat",
|
||||
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Sunting",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Hapus",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Informasi",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Hitung:",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Hapus dan jadikan daftarhitam",
|
||||
"dad95154dcef3509b8cc705046061fd24994bbb7": "dilihat",
|
||||
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Simpan perubahan",
|
||||
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Unduhan telah berhasil",
|
||||
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "Sebuah kesalahan telah terjadi",
|
||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Rincian",
|
||||
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Sebuah kesalahan telah terjadi:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Unduhan dimulai:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Unduhan berakhir:",
|
||||
"ad127117f9471612f47d01eae09709da444a36a4": "Jalur file:",
|
||||
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Langganan anda",
|
||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Saluran",
|
||||
"47546e45bbb476baaaad38244db444c427ddc502": "Daftar putar",
|
||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Nama tidak tersedia. Pengambilan saluran sedang berlangsung.",
|
||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Anda tidak memiliki saluran langganan.",
|
||||
"2e0a410652cb07d069f576b61eab32586a18320d": "Nama tidak tersedia. Pengambilan daftar-putar sedang berlangsung.",
|
||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Anda tidak memiliki langganan daftar putar.",
|
||||
"82421c3e46a0453a70c42900eab51d58d79e6599": "Utama",
|
||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Pengunduh",
|
||||
"d5f69691f9f05711633128b5a3db696783266b58": "Ekstra",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Lanjutan",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Pengguna",
|
||||
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Log",
|
||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Tutup} false {Batal} other {lainnya}}",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL aplikasi ini akan diakses dari, tanpa port.",
|
||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Port",
|
||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Port yang diinginkan. Default-nya adalah 17442.",
|
||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "Mode multi-pengguna",
|
||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Path basis pengguna",
|
||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "Path basis untuk pengguna dan unduhan video mereka.",
|
||||
"4e3120311801c4acd18de7146add2ee4a4417773": "Izinkan langganan",
|
||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Langganan Pangkros Base",
|
||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "Jalur dasar untuk video dari saluran dan daftar putar Anda yang berlangganan. Itu relatif terhadap folder root ytdl-material.",
|
||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Periksa interval",
|
||||
"0f56a7449b77630c114615395bbda4cab398efd8": "Unit adalah detik, hanya termasuk angka.",
|
||||
"13759b09a7f4074ceee8fa2f968f9815fdf63295": "Kadang-kadang video baru diunduh sebelum diproses sepenuhnya. Pengaturan ini akan berarti video baru akan diperiksa untuk versi berkualitas lebih tinggi pada hari berikutnya.",
|
||||
"3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Redownload unggahan segar",
|
||||
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Tema",
|
||||
"ff7cee38a2259526c519f878e71b964f41db4348": "Default",
|
||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Izinkan perubahan tema",
|
||||
"fe46ccaae902ce974e2441abe752399288298619": "Bahasa",
|
||||
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Jalur folder audio",
|
||||
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Jalur untuk unduhan hanya audio. Ini relatif terhadap folder root ytdl-material.",
|
||||
"46826331da1949bd6fb74624447057099c9d20cd": "Jalur folder video",
|
||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Jalur untuk unduhan video. Ini relatif terhadap folder root YTDL-material.",
|
||||
"cfe829634b1144bc44b6d38cf5584ea65db9804f": "Output file default",
|
||||
"1148fd45287ff09955b938756bc302042bcb29c7": "Jalur relatif terhadap jalur unduhan di atas. Jangan sertakan ekstensi.",
|
||||
"ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Args kustom global",
|
||||
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Args kustom global untuk unduhan di beranda. Args dibatasi menggunakan dua koma seperti itu: ,,",
|
||||
"04201f9d27abd7d6f58a4328ab98063ce1072006": "Kategori",
|
||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Gunakan YouTube-DL Archive",
|
||||
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Sertakan thumbnail",
|
||||
"384de8f8f112c9e6092eb2698706d391553f3e8d": "Sertakan metadata",
|
||||
"fb35145bfb84521e21b6385363d59221f436a573": "Bunuh semua unduhan",
|
||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Top judul",
|
||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Manajer file diaktifkan",
|
||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Unduhan Manager diaktifkan",
|
||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Izinkan kualitas pilih",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Unduh Only Mode",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Izinkan Mode Multi-Unduh",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Aktifkan API publik",
|
||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Kunci API Publik",
|
||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Lihat dokumentasi",
|
||||
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Ini akan menghapus kunci API lama Anda!",
|
||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Menghasilkan",
|
||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Gunakan API YouTube",
|
||||
"ce10d31febb3d9d60c160750570310f303a22c22": "YouTube API Key",
|
||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Menghasilkan kunci itu mudah!",
|
||||
"d162f9fcd6a7187b391e004f072ab3da8377c47d": "Gunakan Twitch API",
|
||||
"8ae23bc4302a479f687f4b20a84c276182e2519c": "Kunci Twitch API",
|
||||
"84ffcebac2709ca0785f4a1d5ba274433b5beabc": "Juga dikenal sebagai ID Klien.",
|
||||
"5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Obrolan Download Otomatis",
|
||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Klik disini",
|
||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "Untuk mengunduh ekstensi Chrome Bahan YouTubedl resmi secara manual.",
|
||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Anda harus secara manual memuat ekstensi dan memodifikasi pengaturan ekstensi untuk mengatur URL frontend.",
|
||||
"9a2ec6da48771128384887525bdcac992632c863": "Untuk menginstal ekstensi Firefox Bahan YouTubedl resmi langsung dari halaman ekstensi Firefox.",
|
||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "Instruksi pengaturan terperinci.",
|
||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Tidak banyak yang diperlukan selain mengubah pengaturan ekstensi untuk mengatur URL frontend.",
|
||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Seret tautan di bawah ke bookmark Anda, dan Anda baik-baik saja! Cukup navigasikan ke video YouTube yang ingin Anda unduh, dan klik bookmark.",
|
||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Buat 'audio hanya' Bookmarklet",
|
||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Pilih pengunduh",
|
||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Gunakan agen pengunduhan default",
|
||||
"c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Pilih agen unduhan",
|
||||
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Layar log",
|
||||
"db6c192032f4cab809aad35215f0aa4765761897": "Login kedaluwarsa",
|
||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Izinkan unduhan lanjutan",
|
||||
"431e5f3a0dde88768d1074baedd65266412b3f02": "Gunakan cookie",
|
||||
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Atur Cookie",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Izinkan pendaftaran pengguna",
|
||||
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "Metode Auth",
|
||||
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Intern",
|
||||
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
|
||||
"1db9789b93069861019bd0ccaa5d4706b00afc61": "URL LDAP",
|
||||
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Gabung DN",
|
||||
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Mengikat kredensial",
|
||||
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Basis pencarian",
|
||||
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Cari Filter",
|
||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Tentang YouTubedl-material",
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "Adalah pengunduh YouTube open-source yang dibangun di bawah spesifikasi desain bahan Google. Anda dapat mengunduh video favorit Anda dengan mulus sebagai file video atau audio, dan bahkan berlangganan saluran dan daftar putar favorit Anda untuk terus diperbarui dengan video baru mereka.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "Memiliki beberapa fitur yang luar biasa termasuk! API yang luas, dukungan Docker, dan dukungan pelokalan (terjemahan). Baca pada semua fitur yang didukung dengan mengklik ikon GitHub di atas.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versi yang diinstal:",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "Menemukan bug atau punya saran?",
|
||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "Untuk menciptakan masalah!",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Memeriksa pembaruan ...",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Pembaruan tersedia",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Anda dapat memperbarui dari menu Pengaturan.",
|
||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "Pilih versi:",
|
||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Aktifkan berbagi",
|
||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "Gunakan Timestamp",
|
||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Detik",
|
||||
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Menyalin ke clipboard",
|
||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Bagikan Daftar Putar",
|
||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Berbagi video",
|
||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Bagikan audio",
|
||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID Sesi:",
|
||||
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Hapus semua unduhan",
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(arus)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Tidak ada unduhan yang tersedia!",
|
||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Profil kamu",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Keluar",
|
||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Diciptakan:",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Kamu tidak masuk.",
|
||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Buat Akun Admin",
|
||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Tidak ada akun admin default yang terdeteksi. Ini akan membuat dan mengatur kata sandi untuk akun admin dengan nama pengguna sebagai 'admin'.",
|
||||
"70a67e04629f6d412db0a12d51820b480788d795": "Buat",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Tambahkan pengguna",
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Edit peran",
|
||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Nama pengguna",
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Peran",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Tindakan",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Kelola pengguna",
|
||||
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Hapus pengguna",
|
||||
"632e8b20c98e8eec4059a605a4b011bb476137af": "Edit Pengguna",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "UID pengguna:",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Kata sandi baru",
|
||||
"6498fa1b8f563988f769654a75411bb8060134b9": "Atur kata sandi baru",
|
||||
"544e09cdc99a8978f48521d45f62db0da6dcf742": "Gunakan default peran",
|
||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Ya",
|
||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Tidak",
|
||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Kelola peran",
|
||||
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Baris:",
|
||||
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Hapus log",
|
||||
"24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "Dihasilkan otomatis",
|
||||
"ccf5ea825526ac490974336cb5c24352886abc07": "Buka Berkas",
|
||||
"5656a06f17c24b2d7eae9c221567b209743829a9": "Buka berkas di tab baru",
|
||||
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Menuju ke langganan",
|
||||
"94e01842dcee90531caa52e4147f70679bac87fe": "Hapus dan unduh ulang",
|
||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Hapus selamanya",
|
||||
"ddc31f2885b1b33a7651963254b0c197f2a64086": "Lihat lebih banyak.",
|
||||
"56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Lihat lebih sedikit.",
|
||||
"2054791b822475aeaea95c0119113de3200f5e1c": "Panjang:"
|
||||
}
|
||||
2517
src/assets/i18n/messages.id.xlf
Normal file
2517
src/assets/i18n/messages.id.xlf
Normal file
File diff suppressed because it is too large
Load Diff
261
src/assets/i18n/messages.ko.json
Normal file
261
src/assets/i18n/messages.ko.json
Normal file
@@ -0,0 +1,261 @@
|
||||
{
|
||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "대하여",
|
||||
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "프로필",
|
||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "다크",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "설정",
|
||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "홈",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "로그인",
|
||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "구독",
|
||||
"822fab38216f64e8166d368b59fe756ca39d301b": "다운로드",
|
||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "오디오만",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "다운로드",
|
||||
"a38ae1082fec79ba1f379978337385a539a28e73": "품질",
|
||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "URL 이용",
|
||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "보기",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "복수 다운로드 모드",
|
||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "취소",
|
||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "고급",
|
||||
"4e4c721129466be9c3862294dc40241b64045998": "사용자 지정 인수 이용",
|
||||
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "사용자 지정 인수",
|
||||
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "URL을 포함할 필요가 없습니다. 이후의 모든 항목만 포함하면 됩니다. 인수는 다음과 같은 두 개의 쉼표를 사용하여 구분됩니다. : ,,",
|
||||
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "사용자 지정 출력 사용",
|
||||
"d9c02face477f2f9cdaae318ccee5f89856851fb": "사용자 지정 출력",
|
||||
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "문서",
|
||||
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "경로는 설정된 다운로드 경로에 상대적입니다. 확장자는 포함하지 마세요.",
|
||||
"4e1291cb1d579e7b7a1b802e6a8fd16ef7a557fa": "파일 자르기",
|
||||
"44d007f6f8a2b19f12d85f9e49647b4ac02d7cbe": "자르기 시작지점 (초)",
|
||||
"661206c3ab91fa81e9d8b40afb29f1866b78432f": "자르기 마무리지점 (초)",
|
||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "시뮬레이션된 명령:",
|
||||
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "인증 사용",
|
||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "아이디",
|
||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "비밀번호",
|
||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "재생목록 만들기",
|
||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "제목",
|
||||
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "종류",
|
||||
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "오디오",
|
||||
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "동영상",
|
||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "오디오 파일",
|
||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "동영상",
|
||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "재생목록이나 채널 구독",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
|
||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "재생목록이나 채널 URL",
|
||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "사용자 지정 이름",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "모든 업로드 된 파일 다운로드",
|
||||
"d641b8fa5ac5e85114c733b1f7de6976bd091f70": "최고 화질",
|
||||
"c76a955642714b8949ff3e4b4990864a2e2cac95": "오디오 전용 모드",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "스트리밍 전용 모드",
|
||||
"f432e1a8d6adb12e612127978ce2e0ced933959c": "이것들은 일반적인 인수 뒤에 추가됩니다.",
|
||||
"98b6ec9ec138186d663e64770267b67334353d63": "사용자 지정 파일 출력",
|
||||
"d7b35c384aecd25a516200d6921836374613dfe7": "취소",
|
||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "구독",
|
||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "마지막으로 업로드된 동영상 다운로드",
|
||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "종류:",
|
||||
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "아이디:",
|
||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "닫기",
|
||||
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "아카이브 내보내기",
|
||||
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "구독 취소",
|
||||
"303e45ffae995c9817e510e38cb969e6bb3adcbf": "(일시정지)",
|
||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "아카이브:",
|
||||
"616e206cb4f25bd5885fc35925365e43cf5fb929": "제목:",
|
||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "업로더:",
|
||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "파일 크기:",
|
||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "경로:",
|
||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "업로드 날짜:",
|
||||
"0cc1dec590ecd74bef71a865fb364779bc42a749": "카테고리:",
|
||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Youtube-dl 인수 수정",
|
||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "시뮬레이션된 새 인수",
|
||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "인수 추가",
|
||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "카테고리로 찾기",
|
||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "인수 값 이용",
|
||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "인수 추가",
|
||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "수정",
|
||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "인수 값",
|
||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "업데이터",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "사용자 등록",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "아이디",
|
||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "등록",
|
||||
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "새 쿠키 업로드",
|
||||
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "참고: 새로운 쿠키를 추가하면 이전 쿠키를 덮어씁니다. 또한 쿠키는 사용자 개인이 아닌 전체에 적용됩니다.",
|
||||
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "드래그 앤 드롭",
|
||||
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "재생목록 수정",
|
||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "저장",
|
||||
"cba36d610ddba59b6dd6fbec77199eabf0ff2de3": "재생할 때 재생목록 섞기",
|
||||
"5caadefa4143cf6766a621b0f54f91f373a1f164": "콘텐츠 추가",
|
||||
"33026f57ea65cd9c8a5d917a08083f71a718933a": "기본 순서",
|
||||
"29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "순서 거꾸로",
|
||||
"d02888c485d3aeab6de628508f4a00312a722894": "내 동영상",
|
||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "검색",
|
||||
"73423607944a694ce6f9e55cfee329681bb4d9f9": "동영상 없음.",
|
||||
"3697f8583ea42868aa269489ad366103d94aece7": "수정중",
|
||||
"07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "일시정지됨",
|
||||
"c3b0b86523f1d10e84a71f9b188d54913a11af3b": "카테고리 수정중",
|
||||
"2489eefea00931942b91f4a1ae109514b591e2e1": "규칙",
|
||||
"e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "새로운 규칙 추가",
|
||||
"792dc6a57f28a1066db283f2e736484f066005fd": "트위치 채팅 다운로드",
|
||||
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "수정",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "삭제",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "정보",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "동영상 수:",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "삭제하고 블랙리스트 추가",
|
||||
"dad95154dcef3509b8cc705046061fd24994bbb7": "조회수",
|
||||
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "성공적으로 다운로드 완료",
|
||||
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "에러 발생",
|
||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "세부사항",
|
||||
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "에러 발생:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "다운로드 시작:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "다운로드 끝:",
|
||||
"ad127117f9471612f47d01eae09709da444a36a4": "파일 경로(들):",
|
||||
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "구독중",
|
||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "채널",
|
||||
"47546e45bbb476baaaad38244db444c427ddc502": "재생목록",
|
||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "이름이 유효하지 않음. 채널 검색중.",
|
||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "구독중인 채널이 없습니다.",
|
||||
"2e0a410652cb07d069f576b61eab32586a18320d": "이름이 유효하지 않음. 플레이리스트 검색중.",
|
||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "구독중인 재생목록이 없습니다.",
|
||||
"82421c3e46a0453a70c42900eab51d58d79e6599": "메인",
|
||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "다운로더",
|
||||
"d5f69691f9f05711633128b5a3db696783266b58": "추가",
|
||||
"fb324ec7da611c6283caa6fc6257c39a56d6aaf7": "데이터베이스",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "고급",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "사용자",
|
||||
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "로그",
|
||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {닫다} false {취소} other {기타}}",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "포트를 제외한 이 앱에 접속할 URL.",
|
||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "포트",
|
||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "포트 설정. 기본 포트는 17442 입니다.",
|
||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "복수 사용자 모드",
|
||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "사용자 기본 경로",
|
||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "사용자와 그들의 동영상 다운로드를 위한 기본 경로.",
|
||||
"4e3120311801c4acd18de7146add2ee4a4417773": "구독 허용",
|
||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "구독 기본 경로",
|
||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "구독된 채널과 재생목록에서 나온 영상들을 위한 기본 경로. 경로는 YTDL-Material 루트 폴더 경로에 상대적입니다.",
|
||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "확인 간격",
|
||||
"0f56a7449b77630c114615395bbda4cab398efd8": "단위는 초이며, 숫자만 넣으세요.",
|
||||
"13759b09a7f4074ceee8fa2f968f9815fdf63295": "가끔 새 동영상이 최고 화질 처리 전에 다운로드 될 때가 있습니다. 이 설정은 새 동영상이 더 높은 화질의 버전이 있는지 다음 날짜에 확인됨을 의미합니다.",
|
||||
"3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "높은 화질 재다운로드",
|
||||
"27a56aad79d8b61269ed303f11664cc78bcc2522": "테마",
|
||||
"ff7cee38a2259526c519f878e71b964f41db4348": "기본",
|
||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "테마 변경 허용",
|
||||
"fe46ccaae902ce974e2441abe752399288298619": "언어",
|
||||
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "오디오 폴더 경로",
|
||||
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "오디오 전용 다운로드 경로. 경로는 YTDL-Material 루트 폴더 경로에 상대적입니다.",
|
||||
"46826331da1949bd6fb74624447057099c9d20cd": "동영상 폴더 경로",
|
||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "동영상 다운로드 경로. 경로는 YTDL-Material 루트 폴더 경로에 상대적입니다.",
|
||||
"cfe829634b1144bc44b6d38cf5584ea65db9804f": "기본 파일 출력",
|
||||
"1148fd45287ff09955b938756bc302042bcb29c7": "경로는 위의 다운로드 경로에 상대적입니다. 확장자는 포함하지 마세요.",
|
||||
"ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "전반적으로 적용될 사용자 지정 인수",
|
||||
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "홈페이지에서의 다운로드에 대해 전반적으로 적용될 사용자 지정 인수. 인수는 다음과 같은 두 개의 쉼표를 사용하여 구분됩니다. : ,,",
|
||||
"04201f9d27abd7d6f58a4328ab98063ce1072006": "카테고리",
|
||||
"1f6d3986a970af27f16f8a95ce0dc3033cc90a83": "이 설정을 사용하면, 하나의 동영상이 카테고리와 일치할 경우, 전체 재생목록에 해당 카테고리가 표시됩니다.",
|
||||
"5da94ccb2301f586af26916e921bdad6d673ab58": "재생목록 카테고리화 허용",
|
||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Youtube-dl 아카이브 사용",
|
||||
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "썸네일 포함",
|
||||
"384de8f8f112c9e6092eb2698706d391553f3e8d": "메타데이터 포함",
|
||||
"fb35145bfb84521e21b6385363d59221f436a573": "모든 다운로드 종료",
|
||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "상위 제목",
|
||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "파일 매니저 설정됨",
|
||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "다운로드 매니저 설정됨",
|
||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "화질 선택 허용",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "다운로드 전용 모드",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "복수 다운로드 모드 허용",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "오픈 API 허용",
|
||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "오픈 API 키",
|
||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "문서 보기",
|
||||
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "이것은 예전 API키를 지울 것입니다!",
|
||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "생성",
|
||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "유튜브 API 사용",
|
||||
"ce10d31febb3d9d60c160750570310f303a22c22": "유튜브 API 키",
|
||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "키를 만드는 것은 쉽습니다!",
|
||||
"d162f9fcd6a7187b391e004f072ab3da8377c47d": "트위치 API 사용",
|
||||
"8ae23bc4302a479f687f4b20a84c276182e2519c": "트위치 API 키",
|
||||
"84ffcebac2709ca0785f4a1d5ba274433b5beabc": "클라이언트 ID라고도 알려져 있음.",
|
||||
"5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "트위치 채팅 자동 다운로드",
|
||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "이곳을 누르세요",
|
||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "공식 YoutubeDL-Material 크롬 확장 프로그램을 수동으로 다운로드 하기 위해.",
|
||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "반드시 확장 프로그램을 수동으로 실행하고 확장 프로그램 설정을 수정하여 프론트엔드 URL을 설정해야 합니다.",
|
||||
"9a2ec6da48771128384887525bdcac992632c863": "파이어폭스 확장 프로그램 페이지에서 바로 공식 YoutubeDL-Material 파이어폭스 확장 프로그램을 설치하기 위해.",
|
||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "자세한 설정 지침.",
|
||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "프론트엔드 URL을 설정하기 위해 확장 프로그램 설정을 변경하는 것 외에는 필요한 것이 많지 않습니다.",
|
||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "아래 링크를 북마크에 끌어다 놓으시면 됩니다! 이제 그냥 다운로드하고자 하는 유튜브 비디오 페이지에서 북마크를 클릭하면 됩니다.",
|
||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "'오디오 전용' 북마크 생성",
|
||||
"47955e2cc6986625528b4352034858180d675281": "데이터베이스 위치:",
|
||||
"9f8de81d44ec2a9a58b97e589b9e3154b3966c60": "테이블당 레코드",
|
||||
"3913164a51898aac444bf6c7150e46ad5a8a18ad": "몽고DB 연결 문자열",
|
||||
"5473e36f5102e2ae22ce4c6620cacc40cc98da95": "예시:",
|
||||
"d54142de169844b014ae913a4056c31495f4a305": "연결 문자열 테스트",
|
||||
"98e94c9bdac1ca8beb29d73b2e6f7a9e5e035aec": "DB 전환",
|
||||
"b1c08387975e6feada407c9b5f5f564261b8192b": "데이터베이스 정보를 검색할 수 없습니다. 자세한 내용은 서버 로그를 확인하세요.",
|
||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "다운로더 선택",
|
||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "기본 다운로드 에이전트 사용",
|
||||
"c776eb4992b6c98f58cd89b20c1ea8ac37888521": "다운로드 에이전트 선택",
|
||||
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "로그 레벨",
|
||||
"db6c192032f4cab809aad35215f0aa4765761897": "로그인 만료",
|
||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "고급 다운로드 허용",
|
||||
"431e5f3a0dde88768d1074baedd65266412b3f02": "쿠키 사용",
|
||||
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "쿠키 설정",
|
||||
"635285fa5624d50a408feb7eb564c0db0d3f1ce1": "서버 재시작",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "사용자 등록 허용",
|
||||
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "인증 방법",
|
||||
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "내부",
|
||||
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
|
||||
"1db9789b93069861019bd0ccaa5d4706b00afc61": "LDAP URL",
|
||||
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Bind DN",
|
||||
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Bind Credentials",
|
||||
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "기본 검색",
|
||||
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "검색 필터",
|
||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "YoutubeDL-Material에 대하여",
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "은(는) 구글의 Material 디자인 요건에 따라 만들어진 오픈소스 유튜브 다운로더 입니다. 당신은 당신이 좋아하는 동영상을 동영상이나 오디오 파일로 원활하게 받을 수 있으며, 심지어 당신이 좋아하는 채널이나 재생목록을 구독해 그들의 새로운 동영상을 지속적으로 업데이트 할 수도 있습니다.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "은(는) 광범위한 API, 도커 지원, 현지화 (번역) 지원을 포함한 몇몇 엄청난 기능이 포함되어 있습니다! 아래 깃허브 아이콘을 클릭해 모든 지원되는 기능을 확인해보세요.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "설치된 버전:",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "버그를 찾았거나 제안하실 사항이 있으신가요?",
|
||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "이슈를 생성하기 위해!",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "업데이트 확인중...",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "업데이트 가능",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "설정 메뉴에서 업데이트를 할 수 있습니다.",
|
||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "선택된 버전:",
|
||||
"1f6d14a780a37a97899dc611881e6bc971268285": "공유 허용",
|
||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "타임스탬프 사용",
|
||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "초",
|
||||
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "클립보드에 복사",
|
||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "재생목록 공유",
|
||||
"94e2674467c7a08a291f9bd97ce694d4e47ffd62": "파일 공유",
|
||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "세션 아이디:",
|
||||
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "모든 다운로드된 항목 지우기",
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(현재)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "다운로드된 항목 없음!",
|
||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "프로필",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "로그아웃",
|
||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "생성됨:",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "로그인하지 않았습니다.",
|
||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "관리자 계정 생성",
|
||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "기본 관리자 계정이 감지되지 않았습니다. 이것은 'admin'이라는 ID를 가진 관리자 계정을 만들고, 비밀번호를 설정할 것입니다.",
|
||||
"70a67e04629f6d412db0a12d51820b480788d795": "생성",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "사용자 추가",
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "역할 수정",
|
||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "ID",
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "역할",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "액션",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "사용자 관리",
|
||||
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "사용자 삭제",
|
||||
"632e8b20c98e8eec4059a605a4b011bb476137af": "사용자 수정",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "사용자 UID:",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "새 비밀번호",
|
||||
"6498fa1b8f563988f769654a75411bb8060134b9": "새 비밀번호 설정",
|
||||
"544e09cdc99a8978f48521d45f62db0da6dcf742": "기본 역할 사용",
|
||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "네",
|
||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "아니오",
|
||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "역할 관리",
|
||||
"5009630cdf32ab4f1c78737b9617b8773512c05a": "줄:",
|
||||
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "로그 지우기",
|
||||
"24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "자동으로 생성됨",
|
||||
"ccf5ea825526ac490974336cb5c24352886abc07": "파일 열기",
|
||||
"5656a06f17c24b2d7eae9c221567b209743829a9": "새 탭에서 파일 열기",
|
||||
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "구독중으로 가기",
|
||||
"94e01842dcee90531caa52e4147f70679bac87fe": "삭제하고 재다운로드",
|
||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "영원히 삭제",
|
||||
"ddc31f2885b1b33a7651963254b0c197f2a64086": "더 보기.",
|
||||
"56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "간략히 보기.",
|
||||
"2054791b822475aeaea95c0119113de3200f5e1c": "길이:"
|
||||
}
|
||||
2634
src/assets/i18n/messages.ko.xlf
Normal file
2634
src/assets/i18n/messages.ko.xlf
Normal file
File diff suppressed because it is too large
Load Diff
248
src/assets/i18n/messages.pt.json
Normal file
248
src/assets/i18n/messages.pt.json
Normal file
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Sobre",
|
||||
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Perfil",
|
||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "Escuro",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Configurações",
|
||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Início",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Entrar",
|
||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Inscrições",
|
||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Baixados",
|
||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Apenas áudio",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Baixar",
|
||||
"a38ae1082fec79ba1f379978337385a539a28e73": "Qualidade",
|
||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Usar URL",
|
||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Ver",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Modo baixar-múltiplos",
|
||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Cancelar",
|
||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Avançado",
|
||||
"4e4c721129466be9c3862294dc40241b64045998": "Usar argumentos personalizados",
|
||||
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Argumentos personalizados",
|
||||
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Não é necessário incluir URL, apenas tudo depois. Argumentos são delimitados usando duas vírgulas assim: ,,",
|
||||
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Usar saída personalizada",
|
||||
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Saída personalizada",
|
||||
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentação",
|
||||
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "Caminho é relativo a configuração do caminho de download. Não inclua a extensão.",
|
||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Comando simulado:",
|
||||
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Usar autenticação",
|
||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Nome de usuário",
|
||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Senha",
|
||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Criar lista de reprodução",
|
||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Nome",
|
||||
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Tipo",
|
||||
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Áudio",
|
||||
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Vídeo",
|
||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Arquivos de áudio",
|
||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Vídeos",
|
||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Inscreva-se numa lista de reprodução ou canal",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
|
||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "A URL da lista de reprodução ou canal",
|
||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nome personalizado",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Baixar todos uploads",
|
||||
"d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Qualidade máxima",
|
||||
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Modo apenas-áudio",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Modo somente streaming",
|
||||
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Eles são adicionados após os argumentos padrão.",
|
||||
"98b6ec9ec138186d663e64770267b67334353d63": "Saída de arquivo personalizado",
|
||||
"d7b35c384aecd25a516200d6921836374613dfe7": "Cancelar",
|
||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Inscreva-se",
|
||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Baixe vídeos enviados no último",
|
||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Tipo:",
|
||||
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
|
||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Fechar",
|
||||
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Exportar Arquivos",
|
||||
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Cancelar inscrição",
|
||||
"303e45ffae995c9817e510e38cb969e6bb3adcbf": "(Pausado)",
|
||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Arquivos:",
|
||||
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Nome:",
|
||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Quem subiu:",
|
||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Tamanho do arquivo:",
|
||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Caminho:",
|
||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Data de upload:",
|
||||
"0cc1dec590ecd74bef71a865fb364779bc42a749": "Categoria:",
|
||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modificar argumentos do youtube-dl",
|
||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Novos argumentos simulados",
|
||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "Adicionar um argumento",
|
||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Buscar por categoria",
|
||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Usar valor do argumento",
|
||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Adicionar argumento",
|
||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modificar",
|
||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Valor do argumento",
|
||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Atualizador",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registrar usuário",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Nome de usuário",
|
||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrar",
|
||||
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Fazer upload de novos cookies",
|
||||
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "NOTA: O upload de novos cookies substituirá os cookies anteriores. Observe também que os cookies abrangem toda a instância, não por usuário.",
|
||||
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrastar e soltar",
|
||||
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modificar lista de reprodução",
|
||||
"5caadefa4143cf6766a621b0f54f91f373a1f164": "Adicionar conteúdo",
|
||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Salvar",
|
||||
"33026f57ea65cd9c8a5d917a08083f71a718933a": "Ordem normal",
|
||||
"29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Ordem reversa",
|
||||
"d02888c485d3aeab6de628508f4a00312a722894": "Meus vídeos",
|
||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Buscar",
|
||||
"73423607944a694ce6f9e55cfee329681bb4d9f9": "Nenhum vídeo encontrado.",
|
||||
"3697f8583ea42868aa269489ad366103d94aece7": "Editando",
|
||||
"07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "Em pausa",
|
||||
"c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Editando categoria",
|
||||
"2489eefea00931942b91f4a1ae109514b591e2e1": "Regras",
|
||||
"e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Adicionar nova regra",
|
||||
"792dc6a57f28a1066db283f2e736484f066005fd": "Baixar Twitch Chat",
|
||||
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editar",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Excluir",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Detalhes",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Total:",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Excluir e bloquear",
|
||||
"dad95154dcef3509b8cc705046061fd24994bbb7": "visualizações",
|
||||
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Salvar alterações",
|
||||
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Download finalizado com sucesso",
|
||||
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "Ocorreu um erro",
|
||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Detalhes",
|
||||
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Ocorreu um erro:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Download iniciado:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Download finalizado:",
|
||||
"ad127117f9471612f47d01eae09709da444a36a4": "Destino do arquivo:",
|
||||
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Suas inscrições",
|
||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Canais",
|
||||
"47546e45bbb476baaaad38244db444c427ddc502": "Playlists",
|
||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Nome não disponível. Carregamento do canal em andamento.",
|
||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Você não está inscrito em um canal.",
|
||||
"2e0a410652cb07d069f576b61eab32586a18320d": "Nome não disponível. Carregamento da playlist em andamento.",
|
||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Você não está inscrito em uma playlist.",
|
||||
"82421c3e46a0453a70c42900eab51d58d79e6599": "Geral",
|
||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader",
|
||||
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avançado",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Usuários",
|
||||
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Logs",
|
||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Fechar} false {Cancelar} other {Outro}}",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL para acesso ao app, sem porta.",
|
||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Porta",
|
||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Porta desejada. (Padrão 17442).",
|
||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "Modo multi-usuário",
|
||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Diretório base de usuários",
|
||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "Diretório base para usuários e seus vídeos baixados.",
|
||||
"4e3120311801c4acd18de7146add2ee4a4417773": "Permitir inscrições",
|
||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Diretório base para Inscrições",
|
||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "Diretório base para vídeos das inscrições em canais e playlists. Relativo ao diretório raiz do YTDL-Material.",
|
||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Intervalo de checagem",
|
||||
"0f56a7449b77630c114615395bbda4cab398efd8": "Unidade em segundos, inclua apenas números.",
|
||||
"13759b09a7f4074ceee8fa2f968f9815fdf63295": "Algumas vezes vídeos são baixados antes de serem totalmente processados. Esta configuração faz com que vídeos novos sejam verificados por uma qualidade mais alta no dia seguinte.",
|
||||
"3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Baixar novamente uploads recentes",
|
||||
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Aparência",
|
||||
"ff7cee38a2259526c519f878e71b964f41db4348": "Padrão",
|
||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Permitir alteração da aparência",
|
||||
"fe46ccaae902ce974e2441abe752399288298619": "Idioma",
|
||||
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Diretório para áudio",
|
||||
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Diretório para downloads de 'áudio apenas'. Relativo ao diretório raiz do YTDL-Material.",
|
||||
"46826331da1949bd6fb74624447057099c9d20cd": "Diretório de Vídeos",
|
||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Diretório para download de vídeos. Relativo ao diretório raiz do YTDL-Material.",
|
||||
"cfe829634b1144bc44b6d38cf5584ea65db9804f": "Arquivo de destino padrão",
|
||||
"1148fd45287ff09955b938756bc302042bcb29c7": "O caminho é relativo ao diretório de download acima. Não inclua extensão.",
|
||||
"ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Parâmetro global personalizado",
|
||||
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Parâmetro global personalizado para downloads na home page. Parâmetros são delimitados utilizando duas vírgulas. Ex: ,,",
|
||||
"04201f9d27abd7d6f58a4328ab98063ce1072006": "Categorias",
|
||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Armazenar youtube-dl",
|
||||
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Incluir miniatura",
|
||||
"384de8f8f112c9e6092eb2698706d391553f3e8d": "Incluir metadados",
|
||||
"fb35145bfb84521e21b6385363d59221f436a573": "Parar todos os downloads",
|
||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Título",
|
||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Habilitar gerenciador de arquivos",
|
||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Habilitar gerenciador de downloads",
|
||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Permitir seleção de qualidade",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Modo Apenas Download",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Permitir modo múltiplos downloads",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Habilitar API pública",
|
||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Chave da API Pública",
|
||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Ver documentação",
|
||||
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Isso irá excluir sua chave API anterior!",
|
||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Gerar",
|
||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Usar API do YouTube",
|
||||
"ce10d31febb3d9d60c160750570310f303a22c22": "Chave da API do YouTube",
|
||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Gerar uma chave é fácil!",
|
||||
"d162f9fcd6a7187b391e004f072ab3da8377c47d": "Usar API do Twitch",
|
||||
"8ae23bc4302a479f687f4b20a84c276182e2519c": "Chave da API do Twitch",
|
||||
"84ffcebac2709ca0785f4a1d5ba274433b5beabc": "Também chamado de ID do Cliente (Client ID).",
|
||||
"5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Baixar Twitch Chat automaticamente",
|
||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Clique aqui",
|
||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "para baixar a extensão do YoutubeDL-Material para o Chrome manualmente.",
|
||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Você deve carregar a extensão manualmente e alterar as configurações para a URL inicial.",
|
||||
"9a2ec6da48771128384887525bdcac992632c863": "para instalar a extensão oficial do YoutubeDL-Material para o Firefox diretamente da página de extensões.",
|
||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "Instruções de instalação detalhadas.",
|
||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Nada além de alterar a configuração da extensão para a URL inicial é necessário.",
|
||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Arraste o link abaixo para seus Favoritos, e pronto! Navegue para o vídeo do Youtube que deseja baixar e clique no link favoritado.",
|
||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Gerar favorito 'apenas áudio' interativo",
|
||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Selecione um downloader",
|
||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Usar agente de download padrão",
|
||||
"c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Selecionar um agente de download",
|
||||
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Nível do Log",
|
||||
"db6c192032f4cab809aad35215f0aa4765761897": "Expiração do login",
|
||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Permitir Download avançado",
|
||||
"431e5f3a0dde88768d1074baedd65266412b3f02": "Usar Cookies",
|
||||
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Configurar Cookies",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Permitir cadastro de usuário",
|
||||
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "Método de autenticação",
|
||||
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Interno",
|
||||
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
|
||||
"1db9789b93069861019bd0ccaa5d4706b00afc61": "URL do LDAP",
|
||||
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Bind DN",
|
||||
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Bind Credentials",
|
||||
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Search Base",
|
||||
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Search Filter",
|
||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Sobre o YoutubeDL-Material",
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "é um downloader open-source para o Youtube feito nas especificações do Mateerial Design da Google. Você pode baixar seus vídeos favoritos como arquivos de vídeo ou áudio, e até se inscrever nos seus canais ou playlists favoritos para ficar atualizado com os novos vídeos publicados.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "tem algumas funcionalidades incríveis inclusas! Uma API extensa, suporte ao Docker, suporte à tradução. Leia sobre todas as funcionalidades suportadas clicando no ícone do GitHub acima.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versão instalada:",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "Achou m bug ou tem uma sugestão?",
|
||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "para criar uma ocorrência!",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Buscando por atualizações...",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Atualização disponível",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Você pode atualizar pelo Menu de Configuração.",
|
||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "Selecionar uma versão:",
|
||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Habilitar compartilhamento",
|
||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "Usar timestamp",
|
||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Segundos",
|
||||
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Copiar para a Área de transferência",
|
||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Compartilhar playlist",
|
||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Compartilhar Vídeo",
|
||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Compartilhar Áudio",
|
||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID da sessão:",
|
||||
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Limpar todos os downloads",
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(atual)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Não há downloads disponíveis!",
|
||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Seu Perfil",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Sair",
|
||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Criado:",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Você não está logado.",
|
||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Criar conta de Administrador",
|
||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Conta padrão de Administrador não detectada. Isto irá criar e configurar uma senha para a conta de administrador com o usuário 'admin'.",
|
||||
"70a67e04629f6d412db0a12d51820b480788d795": "Criar",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Adicionar Usuários",
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar função",
|
||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Nome de usuário",
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Função",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Ações",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Gerenciar usuário",
|
||||
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Excluir usuário",
|
||||
"632e8b20c98e8eec4059a605a4b011bb476137af": "Editar usuário",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "UID do usuário:",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nova senha",
|
||||
"6498fa1b8f563988f769654a75411bb8060134b9": "Aplicar nova senha",
|
||||
"544e09cdc99a8978f48521d45f62db0da6dcf742": "Usar função padrão",
|
||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Sim",
|
||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Não",
|
||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Gerenciar função",
|
||||
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Linhas:",
|
||||
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Limpar logs",
|
||||
"24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "Auto-gerado",
|
||||
"ccf5ea825526ac490974336cb5c24352886abc07": "Abrir arquivo",
|
||||
"5656a06f17c24b2d7eae9c221567b209743829a9": "Abrir arquivo em uma nova aba",
|
||||
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Ir para Inscrições",
|
||||
"94e01842dcee90531caa52e4147f70679bac87fe": "Excluir e baixar novamente",
|
||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Excluir para sempre",
|
||||
"ddc31f2885b1b33a7651963254b0c197f2a64086": "Ver mais.",
|
||||
"56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Ver menos.",
|
||||
"2054791b822475aeaea95c0119113de3200f5e1c": "Duração:"
|
||||
}
|
||||
2517
src/assets/i18n/messages.pt.xlf
Normal file
2517
src/assets/i18n/messages.pt.xlf
Normal file
File diff suppressed because it is too large
Load Diff
268
src/assets/i18n/messages.ru.json
Normal file
268
src/assets/i18n/messages.ru.json
Normal file
@@ -0,0 +1,268 @@
|
||||
{
|
||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Создайте список воспроизведения",
|
||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Имя",
|
||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Аудиофайлы",
|
||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Видео",
|
||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Изменение аргументов youtube-dl",
|
||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Моделирование новых аргументов",
|
||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "Добавить аргумент",
|
||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Поиск по категориям",
|
||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Использовать значение аргумента",
|
||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Значение аргумента",
|
||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Добавить аргумент",
|
||||
"d7b35c384aecd25a516200d6921836374613dfe7": "Отмена",
|
||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Модифицировать",
|
||||
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "",
|
||||
"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": "Загрузка новых файлов cookie",
|
||||
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Перетаскивание",
|
||||
"85e0725c870b28458fd3bbba905397d890f00a69": "",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Настройки",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL-адрес",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL, с которого будет осуществляться доступ к этому приложению, без указания порта.",
|
||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Порт",
|
||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Желаемый порт. По умолчанию - 17442.",
|
||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "Многопользовательский режим",
|
||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Базовый путь пользователей",
|
||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "Базовый путь для пользователей и их загруженных видео.",
|
||||
"cbe16a57be414e84b6a68309d08fad894df797d6": "",
|
||||
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "",
|
||||
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "",
|
||||
"4e3120311801c4acd18de7146add2ee4a4417773": "Разрешить подписки",
|
||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Базовый путь подписки",
|
||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "Базовый путь для видео из подписанных вами каналов и плейлистов. Он является относительным к корневой папке YTDL-Material.",
|
||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Контрольный интервал",
|
||||
"0f56a7449b77630c114615395bbda4cab398efd8": "Единица измерения - секунды, включайте только цифры.",
|
||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Используйте архив youtube-dl",
|
||||
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "",
|
||||
"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": "",
|
||||
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Включить публичный API",
|
||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Открытый ключ API",
|
||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Просмотр документации",
|
||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Создать",
|
||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Используйте API YouTube",
|
||||
"ce10d31febb3d9d60c160750570310f303a22c22": "Ключ API Youtube",
|
||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Сгенерировать ключ очень просто!",
|
||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Нажмите здесь",
|
||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "чтобы загрузить официальное расширение YoutubeDL-Material Chrome вручную.",
|
||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Вы должны вручную загрузить расширение и изменить его настройки, чтобы установить URL-адрес фронтенда.",
|
||||
"9a2ec6da48771128384887525bdcac992632c863": "чтобы установить официальное расширение YoutubeDL-Material Firefox прямо со страницы расширений Firefox.",
|
||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "Подробные инструкции по настройке.",
|
||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Не требуется ничего особенного, кроме изменения настроек расширения для установки внешнего URL.",
|
||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Перетащите приведенную ниже ссылку в закладки, и все готово! Просто перейдите к видео YouTube, которое вы хотите загрузить, и нажмите на закладку.",
|
||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Создать букмарклет \"только аудио\"",
|
||||
"d5f69691f9f05711633128b5a3db696783266b58": "Дополнительно",
|
||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Использовать агент загрузки по умолчанию",
|
||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Выберите программу загрузки",
|
||||
"00e274c496b094a019f0679c3fab3945793f3335": "",
|
||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Разрешить расширенную загрузку",
|
||||
"431e5f3a0dde88768d1074baedd65266412b3f02": "Использование файлов cookie",
|
||||
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Установить Cookies",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Продвинутый",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Разрешить регистрацию пользователей",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Пользователи",
|
||||
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Журналы",
|
||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Сохранить",
|
||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Close} false {Cancel} other {otha}}",
|
||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "О YoutubeDL-Material",
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "это загрузчик YouTube с открытым исходным кодом, созданный в соответствии со спецификациями Material Design от Google. Вы можете легко загружать любимые видеоролики в виде видео- или аудиофайлов и даже подписываться на любимые каналы и плейлисты, чтобы быть в курсе их новых видео.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "включает в себя несколько потрясающих функций! Обширный API, поддержка Docker и поддержка локализации (перевода). Ознакомьтесь со всеми поддерживаемыми функциями, нажав на значок GitHub выше.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Установленная версия:",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Проверяем обновления...",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Доступно обновление",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Обновление можно выполнить в меню настроек.",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "Нашли ошибку или у вас есть предложение?",
|
||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "чтобы создать проблему!",
|
||||
"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": "Идентификатор сессии:",
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(текущий)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Загрузка недоступна!",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Зарегистрировать пользователя",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Имя пользователя",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Управление пользователями",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Идентификатор пользователя:",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Новый пароль",
|
||||
"6498fa1b8f563988f769654a75411bb8060134b9": "Установите новый пароль",
|
||||
"40da072004086c9ec00d125165da91eaade7f541": "",
|
||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Да",
|
||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Нет",
|
||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Управлять ролью",
|
||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Имя пользователя",
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Роль",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Действия",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Добавить пользователей",
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Редактировать роль",
|
||||
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "",
|
||||
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Линии:",
|
||||
"56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Смотреть меньше.",
|
||||
"ddc31f2885b1b33a7651963254b0c197f2a64086": "См. подробнее.",
|
||||
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Перейти к подписке",
|
||||
"5656a06f17c24b2d7eae9c221567b209743829a9": "Открыть файл в новой вкладке",
|
||||
"ccf5ea825526ac490974336cb5c24352886abc07": "Открыть файл",
|
||||
"24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "Автогенерируемый",
|
||||
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Очистить журналы",
|
||||
"544e09cdc99a8978f48521d45f62db0da6dcf742": "Использовать роль по умолчанию",
|
||||
"632e8b20c98e8eec4059a605a4b011bb476137af": "Редактировать пользователя",
|
||||
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Удалить пользователя",
|
||||
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Очистить все загрузки",
|
||||
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Фильтр поиска",
|
||||
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "База поиска",
|
||||
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Связать учетные данные",
|
||||
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Привязать DN",
|
||||
"1db9789b93069861019bd0ccaa5d4706b00afc61": "URL-адрес LDAP",
|
||||
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
|
||||
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Внутренний",
|
||||
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "Метод авторизации",
|
||||
"db6c192032f4cab809aad35215f0aa4765761897": "Истечение срока действия логина",
|
||||
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Уровень журнала",
|
||||
"c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Выберите агент загрузки",
|
||||
"5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Автоматическая загрузка чата Twitch",
|
||||
"84ffcebac2709ca0785f4a1d5ba274433b5beabc": "Также известен как идентификатор клиента.",
|
||||
"8ae23bc4302a479f687f4b20a84c276182e2519c": "Ключ API Twitch",
|
||||
"d162f9fcd6a7187b391e004f072ab3da8377c47d": "Используйте API Twitch",
|
||||
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Это удалит ваш старый ключ API!",
|
||||
"fb35145bfb84521e21b6385363d59221f436a573": "Убить все загрузки",
|
||||
"384de8f8f112c9e6092eb2698706d391553f3e8d": "Включить метаданные",
|
||||
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Включить уменьшенное изображение",
|
||||
"04201f9d27abd7d6f58a4328ab98063ce1072006": "Категории",
|
||||
"ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Глобальные пользовательские аргументы",
|
||||
"1148fd45287ff09955b938756bc302042bcb29c7": "Путь является относительным по отношению к вышеуказанным путям загрузки. Не включайте расширение.",
|
||||
"cfe829634b1144bc44b6d38cf5584ea65db9804f": "Вывод файлов по умолчанию",
|
||||
"3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Повторная загрузка свежих загрузок",
|
||||
"13759b09a7f4074ceee8fa2f968f9815fdf63295": "Иногда новые видео загружаются до полной обработки. Эта настройка означает, что новые видео будут проверяться на наличие версии более высокого качества на следующий день.",
|
||||
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "Произошла ошибка",
|
||||
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Загрузка прошла успешно",
|
||||
"dad95154dcef3509b8cc705046061fd24994bbb7": "просмотры",
|
||||
"792dc6a57f28a1066db283f2e736484f066005fd": "Скачать Чат Twitch",
|
||||
"e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Добавить новое правило",
|
||||
"2489eefea00931942b91f4a1ae109514b591e2e1": "Правила",
|
||||
"c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Категория редактирования",
|
||||
"07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "Приостановлено",
|
||||
"3697f8583ea42868aa269489ad366103d94aece7": "Редактирование",
|
||||
"73423607944a694ce6f9e55cfee329681bb4d9f9": "Видео не найдено.",
|
||||
"d02888c485d3aeab6de628508f4a00312a722894": "Мои видео",
|
||||
"29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Обратный порядок",
|
||||
"33026f57ea65cd9c8a5d917a08083f71a718933a": "Обычный порядок",
|
||||
"5caadefa4143cf6766a621b0f54f91f373a1f164": "Добавить содержание",
|
||||
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "ПРИМЕЧАНИЕ: Загрузка новых файлов cookie отменяет предыдущие файлы cookie. Также обратите внимание, что файлы cookie используются в масштабах всего экземпляра, а не каждого пользователя.",
|
||||
"0cc1dec590ecd74bef71a865fb364779bc42a749": "Категория:",
|
||||
"303e45ffae995c9817e510e38cb969e6bb3adcbf": "(Пауза)",
|
||||
"d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Максимальное качество",
|
||||
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Видео",
|
||||
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Аудио",
|
||||
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Тип"
|
||||
}
|
||||
2589
src/assets/i18n/messages.ru.xlf
Normal file
2589
src/assets/i18n/messages.ru.xlf
Normal file
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,10 @@ $dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn);
|
||||
@include angular-material-theme($dark-theme);
|
||||
}
|
||||
|
||||
.mat-stroked-button, .mat-raised-button, .mat-flat-button {
|
||||
border-radius: 24px !important
|
||||
}
|
||||
|
||||
// Light theme
|
||||
$light-primary: mat-palette($mat-grey, 200, 500, 300);
|
||||
$light-accent: mat-palette($mat-brown, 200);
|
||||
@@ -50,7 +54,7 @@ $light-warn: mat-palette($mat-deep-orange, 200);
|
||||
$light-theme: mat-light-theme($light-primary, $light-accent, $light-warn);
|
||||
|
||||
.light-theme {
|
||||
@include angular-material-theme($light-theme)
|
||||
@include angular-material-theme($light-theme);
|
||||
}
|
||||
|
||||
.no-outline {
|
||||
|
||||
Reference in New Issue
Block a user