Compare commits

...

47 Commits

Author SHA1 Message Date
Isaac Abadi
c9c0247480 Tasks actions styling update 2023-05-21 00:12:58 -06:00
Isaac Abadi
2fcf5364d8 Rebuild database task now asks for confirmation before rebuilding
Fixed api types build errors
2023-05-21 00:11:48 -06:00
Isaac Abadi
a38fb0e2e0 Updated api models 2023-05-20 23:49:27 -06:00
Isaac Abadi
e6050969ec Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into rebuild-database-task 2023-05-20 23:44:49 -06:00
Isaac Abadi
958300c281 Added typing to task key 2023-05-20 23:44:14 -06:00
Tzahi12345
84c2b2769b Merge pull request #890 from martadinata666/js-generate
docker : reduce build time and size
2023-05-21 00:35:28 -04:00
Isaac Abadi
e145c9c992 Updated fetch-twitchdownloader.sh to get the latest release for the specific arch 2023-05-20 19:32:00 -06:00
Isaac Abadi
078408236c Code cleanup
Default user password after rebuild is now 'password'
2023-05-20 18:27:08 -06:00
Dedy Martadinata S
2adbc0a02c Update fetch-twitchdownloader.sh 2023-05-20 11:35:12 +07:00
Dedy Martadinata S
fe95f04c18 Update Dockerfile 2023-05-20 11:30:24 +07:00
Dedy Martadinata S
9b3816afce Update Dockerfile 2023-05-20 11:25:16 +07:00
Dedy Martadinata S
07874d9241 Revert 142d708ee3
It become fail to set anything
2023-05-20 11:13:09 +07:00
Isaac Abadi
03122b4c81 Subscription metadata is now backed up
Rebuild database now used subscription metadata backup
2023-05-18 23:32:12 -04:00
Isaac Abadi
3deb1e8459 updated subscriptions.js export syntax 2023-05-18 22:23:48 -04:00
Dedy Martadinata S
9fa1aab1e5 Update Dockerfile, Update CI PR, use scripts to download twitchdownloader 2023-05-12 14:46:27 +07:00
Glassed Silver
80b41af620 Merge pull request #910 from Tzahi12345/twitch-chat-fix
Twitch chat downloader fix for Docker
2023-05-12 07:01:12 +02:00
Tzahi12345
ab5d8dc5ca Merge pull request #911 from Tzahi12345/registration-fixes
Registration fixes
2023-05-12 00:03:05 -04:00
Tzahi12345
4b55c39f39 permissions code simplified 2023-05-11 23:14:40 -04:00
Tzahi12345
3ca296f195 Fixed compilation issue 2023-05-11 23:14:25 -04:00
Tzahi12345
d4fa640f0f Added tasks_manager to possible user/role permission in openapi 2023-05-11 02:47:06 -04:00
Tzahi12345
427eecf214 Fixed issue where after resetting the DB, admin would have to be registered twice 2023-05-11 02:44:05 -04:00
Tzahi12345
4f54e408a5 Removed erroneous error after registering admin 2023-05-11 02:43:37 -04:00
Tzahi12345
9e481bbd5f Fixed issue where twitch chat downloader could not be found in docker 2023-05-11 02:27:35 -04:00
Glassed Silver
78b29a76b8 Merge pull request #864 from nardis556/master
Update entrypoint.sh
2023-05-11 03:19:50 +02:00
Glassed Silver
0342d18f76 Merge pull request #906 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2023-05-11 03:17:40 +02:00
Glassed Silver
70754c580c Merge pull request #908 from Tzahi12345/ffmpeg-force-v5
Force ffmpeg 5.1.1
2023-05-11 03:16:08 +02:00
Tzahi12345
e58b0b8638 Force ffmpeg 5.1.1 2023-05-10 19:04:48 -04:00
YMisterXY
df8f8070ca Translated using Weblate (Polish)
Currently translated at 80.5% (385 of 478 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pl/
2023-05-10 13:49:19 +02:00
gallegonovato
0b8ca31594 Translated using Weblate (Spanish)
Currently translated at 100.0% (478 of 478 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2023-05-10 13:49:19 +02:00
Tzahi12345
658a76dc1c Added missing admin tasks_manager role 2023-05-08 19:23:07 -04:00
Glassed Silver
f363ec5db6 Merge pull request #901 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2023-05-08 22:12:01 +02:00
gallegonovato
f36d675abf Translated using Weblate (Spanish)
Currently translated at 100.0% (480 of 480 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2023-05-08 17:52:23 +02:00
Tzahi12345
35fcf44e1a Fixed rebuild db log messages
Rebuild db now imports unregistered files
2023-05-08 02:08:01 -04:00
Tzahi12345
bad6080730 Adds task to rebuild database 2023-05-08 02:00:43 -04:00
Tzahi12345
2a7b62272e Merge remote-tracking branch 'origin/testing-improvements' 2023-05-07 23:36:35 -04:00
Tzahi12345
be74377a08 Merge pull request #899 from Tzahi12345/remove-armv7
Remove armv7 support in Docker
2023-05-07 02:13:55 -04:00
Isaac Abadi
808c7e2112 Temporarily remove armv7 support
Revert "Added python3.8-dev/build-essential to dockerfile"

This reverts commit d90434c240.

Revert "Adds token to GH actions for GetTwitchDownloader"

This reverts commit a4ca1abb7c.
2023-05-07 01:23:58 -04:00
Isaac Abadi
d6f39d37b5 Added PR multiarch
Added python3.8-dev/build-essential to dockerfile

Adds token to GH actions for GetTwitchDownloader
2023-05-07 01:23:44 -04:00
Isaac Abadi
0c46b044da Improved tests for multi-user mode 2023-05-06 23:29:20 -04:00
Tzahi12345
e573f34cea Merge pull request #893 from Tzahi12345/readme-update
README update
2023-05-05 00:05:03 -04:00
Tzahi12345
52e32d4f0f Changed tcd to Twitch Downloader 2023-05-04 23:44:12 -04:00
Tzahi12345
adb5f2256e Translations source file update 2023-05-04 22:33:48 -04:00
Tzahi12345
59bf6ff86d Merge pull request #888 from Tzahi12345/dependency-updates
Dependency updates
2023-05-04 22:28:50 -04:00
Tzahi12345
68fbde8907 Merge pull request #800 from Bastians-Bits/master
Development Documentation
2023-05-03 19:32:07 -04:00
Tzahi12345
62bccb3349 Updated DEVELOPMENT.md to reflect dev config file 2023-05-03 17:01:17 -04:00
nardis556
26988bd607 Update entrypoint.sh 2023-04-16 15:03:12 -05:00
bastiansbits
575f7eed4e Added a new read me (DEVELOPMENT.md) as starting point for new develope
Added a new VSC launch configuration to start the backend in the debugger
Update the build instruction in README.md (Issue #728)
2022-12-07 14:43:43 +01:00
158 changed files with 1609 additions and 470 deletions

View File

@@ -23,5 +23,16 @@ jobs:
name: "version.json" name: "version.json"
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}' json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/' dir: 'backend/'
- name: Build docker images - name: setup platform emulator
run: docker build . -t tzahi12345/youtubedl-material:nightly-pr uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1
- name: build & push images
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
#platforms: linux/amd64
push: false
tags: tzahi12345/youtubedl-material:nightly-pr

View File

@@ -80,7 +80,7 @@ jobs:
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
push: true push: true
tags: ${{ steps.docker-meta.outputs.tags }} tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }} labels: ${{ steps.docker-meta.outputs.labels }}

View File

@@ -80,7 +80,7 @@ jobs:
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
push: true push: true
tags: ${{ steps.docker-meta.outputs.tags }} tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }} labels: ${{ steps.docker-meta.outputs.labels }}

14
.vscode/launch.json vendored
View File

@@ -4,6 +4,20 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "Dev: Debug Backend",
"request": "launch",
"runtimeArgs": [
"run-script",
"debug"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"cwd": "${workspaceFolder}/backend"
},
{ {
"type": "node", "type": "node",
"request": "attach", "request": "attach",

38
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,38 @@
<h1>Development</h1>
- [First time...](#first-time)
- [Setup](#setup)
- [Startup](#startup)
- [Debugging the backend (VSC)](#debugging-the-backend-vsc)
- [Deploy changes](#deploy-changes)
- [Frontend](#frontend)
- [Backend](#backend)
# First time...
## Setup
Checkout the repository and navigate to the `youtubedl-material` directory.
```bash
vim ./src/assets/default.json # Edit settings for your local environment. This config file is just the dev config file, if YTDL_MODE is not set to "debug", then ./backend/appdata/default.json will be used
npm -g install pm2 # Install pm2
npm install # Install dependencies for the frontend
cd ./backend
npm install # Install dependencies for the backend
cd ..
npm run build # Build the frontend
```
This step have to be done only once.
## Startup
Navigate to the `youtubedl-material/backend` directory and run `npm start`.
# Debugging the backend (VSC)
Open the `youtubedl-material` directory in Visual Studio Code and run the launch configuration `Dev: Debug Backend`.
# Deploy changes
## Frontend
Navigate to the `youtubedl-material` directory and run `npm run build`. Restart the backend.
## Backend
Simply restart the backend.

View File

@@ -1,15 +1,17 @@
# Fetching our ffmpeg # Fetching our utils
FROM ubuntu:22.04 AS ffmpeg FROM ubuntu:22.04 AS utils
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
# Use script due local build compability # Use script due local build compability
COPY docker-utils/ffmpeg-fetch.sh . COPY docker-utils/*.sh .
RUN chmod +x ffmpeg-fetch.sh RUN chmod +x *.sh
RUN sh ./ffmpeg-fetch.sh RUN sh ./ffmpeg-fetch.sh
RUN sh ./fetch-twitchdownloader.sh
# Create our Ubuntu 22.04 with node 16.14.2 (that specific version is required as per: https://stackoverflow.com/a/72855258/8088021) # Create our Ubuntu 22.04 with node 16.14.2 (that specific version is required as per: https://stackoverflow.com/a/72855258/8088021)
# Go to 20.04 # Go to 20.04
FROM ubuntu:20.04 AS base FROM ubuntu:22.04 AS base
ARG TARGETPLATFORM
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV UID=1000 ENV UID=1000
ENV GID=1000 ENV GID=1000
@@ -17,19 +19,30 @@ ENV USER=youtube
ENV NO_UPDATE_NOTIFIER=true ENV NO_UPDATE_NOTIFIER=true
ENV PM2_HOME=/app/pm2 ENV PM2_HOME=/app/pm2
ENV ALLOW_CONFIG_MUTATIONS=true ENV ALLOW_CONFIG_MUTATIONS=true
# Directy fetch specific version
## https://deb.nodesource.com/node_16.x/pool/main/n/nodejs/nodejs_16.14.2-deb-1nodesource1_amd64.deb
RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \ RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
apt update && \ apt update && \
apt install -y --no-install-recommends curl ca-certificates tzdata && \ apt install -y --no-install-recommends curl ca-certificates tzdata libicu70 && \
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
apt install -y --no-install-recommends nodejs && \
npm -g install npm n && \
n 16.14.2 && \
apt clean && \ apt clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN case ${TARGETPLATFORM} in \
"linux/amd64") NODE_ARCH=amd64 ;; \
"linux/arm") NODE_ARCH=armhf ;; \
"linux/arm/v7") NODE_ARCH=armhf ;; \
"linux/arm64") NODE_ARCH=arm64 ;; \
esac \
&& curl -L https://deb.nodesource.com/node_16.x/pool/main/n/nodejs/nodejs_16.14.2-deb-1nodesource1_$NODE_ARCH.deb -o ./nodejs.deb && \
apt update && \
apt install -y ./nodejs.deb && \
apt clean && \
rm -rf /var/lib/apt/lists/* &&\
rm nodejs.deb;
# Build frontend # Build frontend
FROM base as frontend ARG BUILDPLATFORM
FROM --platform=${BUILDPLATFORM} node:16 as frontend
RUN npm install -g @angular/cli RUN npm install -g @angular/cli
WORKDIR /build WORKDIR /build
COPY [ "package.json", "package-lock.json", "angular.json", "tsconfig.json", "/build/" ] COPY [ "package.json", "package-lock.json", "angular.json", "tsconfig.json", "/build/" ]
@@ -49,32 +62,35 @@ RUN npm config set strict-ssl false && \
npm install --prod && \ npm install --prod && \
ls -al ls -al
FROM base as python #FROM base as python
WORKDIR /app # armv7 need build from source
COPY docker-utils/GetTwitchDownloader.py . #WORKDIR /app
RUN apt update && \ #COPY docker-utils/GetTwitchDownloader.py .
apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip && \ #RUN apt update && \
apt clean && \ # apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip python3-dev build-essential libffi-dev && \
rm -rf /var/lib/apt/lists/* # apt clean && \
RUN pip install PyGithub requests # rm -rf /var/lib/apt/lists/*
RUN python GetTwitchDownloader.py #RUN pip install PyGithub requests
#RUN python GetTwitchDownloader.py
# Final image # Final image
FROM base FROM base
RUN npm install -g pm2 && \ RUN npm install -g pm2 && \
apt update && \ apt update && \
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \ apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
pip install pycryptodomex && \
apt remove -y --purge build-essential && \
apt autoremove -y --purge && \
apt clean && \ apt clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN pip install pycryptodomex
WORKDIR /app WORKDIR /app
# User 1000 already exist from base image # User 1000 already exist from base image
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ] COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ] COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/TwitchDownloaderCLI", "/usr/local/bin/TwitchDownloaderCLI"]
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"] COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ] COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
COPY --chown=$UID:$GID --from=python ["/app/TwitchDownloaderCLI","/usr/local/bin/TwitchDownloaderCLI"] #COPY --chown=$UID:$GID --from=python ["/app/TwitchDownloaderCLI","/usr/local/bin/TwitchDownloaderCLI"]
RUN chown $UID:$GID .
RUN chmod +x /app/fix-scripts/*.sh RUN chmod +x /app/fix-scripts/*.sh
# Add some persistence data # Add some persistence data
#VOLUME ["/app/appdata"] #VOLUME ["/app/appdata"]

View File

@@ -1758,14 +1758,14 @@ components:
type: object type: object
properties: properties:
task_key: task_key:
type: string $ref: '#/components/schemas/TaskType'
required: required:
- task_key - task_key
UpdateTaskScheduleRequest: UpdateTaskScheduleRequest:
type: object type: object
properties: properties:
task_key: task_key:
type: string $ref: '#/components/schemas/TaskType'
new_schedule: new_schedule:
$ref: '#/components/schemas/Schedule' $ref: '#/components/schemas/Schedule'
required: required:
@@ -1775,7 +1775,7 @@ components:
type: object type: object
properties: properties:
task_key: task_key:
type: string $ref: '#/components/schemas/TaskType'
new_data: new_data:
type: object type: object
required: required:
@@ -1785,7 +1785,7 @@ components:
type: object type: object
properties: properties:
task_key: task_key:
type: string $ref: '#/components/schemas/TaskType'
new_options: new_options:
type: object type: object
required: required:
@@ -2726,7 +2726,7 @@ components:
type: object type: object
properties: properties:
key: key:
type: string $ref: '#/components/schemas/TaskType'
title: title:
type: string type: string
last_ran: last_ran:
@@ -2742,9 +2742,20 @@ components:
error: error:
type: string type: string
schedule: schedule:
type: object $ref: '#/components/schemas/Schedule'
options: options:
type: object type: object
TaskType:
type: string
enum:
- backup_local_db
- missing_files_check
- missing_db_records
- duplicate_files_check
- youtubedl_update_check
- delete_old_files
- import_legacy_archives
- rebuild_database
Schedule: Schedule:
required: required:
- type - type
@@ -2877,6 +2888,7 @@ components:
- sharing - sharing
- advanced_download - advanced_download
- downloads_manager - downloads_manager
- tasks_manager
YesNo: YesNo:
type: string type: string
enum: enum:

View File

@@ -36,8 +36,7 @@ Required dependencies:
Optional dependencies: Optional dependencies:
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`) * AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
* [tcd](https://github.com/PetterKraabol/Twitch-Chat-Downloader) (for downloading Twitch VOD chats) * [Twitch Downloader CLI](https://github.com/lay295/TwitchDownloader) (for downloading Twitch VOD chats)
<details> <details>
<summary>Debian/Ubuntu</summary> <summary>Debian/Ubuntu</summary>
@@ -86,7 +85,9 @@ If you'd like to install YoutubeDL-Material, go to the Installation section. If
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend. To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `npm build`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder. Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `npm run build`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
Lastly, type `npm -g install pm2` to install pm2 globally.
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`. The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.

View File

@@ -535,6 +535,7 @@ async function loadConfig() {
if (allowSubscriptions) { if (allowSubscriptions) {
// set downloading to false // set downloading to false
let subscriptions = await subscriptions_api.getAllSubscriptions(); let subscriptions = await subscriptions_api.getAllSubscriptions();
subscriptions.forEach(async sub => subscriptions_api.writeSubscriptionMetadata(sub));
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false}); subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false});
// runs initially, then runs every ${subscriptionCheckInterval} seconds // runs initially, then runs every ${subscriptionCheckInterval} seconds
const watchSubscriptionsInterval = function() { const watchSubscriptionsInterval = function() {
@@ -1928,9 +1929,34 @@ app.post('/api/clearAllLogs', optionalJwt, async function(req, res) {
// user authentication // user authentication
app.post('/api/auth/register' app.post('/api/auth/register', optionalJwt, async (req, res) => {
, optionalJwt const userid = req.body.userid;
, auth_api.registerUser); const username = req.body.username;
const plaintextPassword = req.body.password;
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
res.sendStatus(409);
return;
}
if (plaintextPassword === "") {
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
res.sendStatus(409);
return;
}
const new_user = await auth_api.registerUser(userid, username, plaintextPassword);
if (!new_user) {
res.sendStatus(409);
return;
}
res.send({
user: new_user
});
});
app.post('/api/auth/login' app.post('/api/auth/login'
, auth_api.passport.authenticate(['local', 'ldapauth'], {}) , auth_api.passport.authenticate(['local', 'ldapauth'], {})
, auth_api.generateJWT , auth_api.generateJWT
@@ -1982,18 +2008,7 @@ app.post('/api/updateUser', optionalJwt, async (req, res) => {
app.post('/api/deleteUser', optionalJwt, async (req, res) => { app.post('/api/deleteUser', optionalJwt, async (req, res) => {
let uid = req.body.uid; let uid = req.body.uid;
try { try {
let success = false; const success = await auth_api.deleteUser(uid);
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const user_folder = path.join(__dirname, usersFileFolder, uid);
const user_db_obj = await db_api.getRecord('users', {uid: uid});
if (user_db_obj) {
// user exists, let's delete
await fs.remove(user_folder);
await db_api.removeRecord('users', {uid: uid});
success = true;
} else {
logger.error(`Could not find user with uid ${uid}`);
}
res.send({success: success}); res.send({success: success});
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);

View File

@@ -1,11 +1,13 @@
const config_api = require('../config'); const config_api = require('../config');
const consts = require('../consts'); const CONSTS = require('../consts');
const logger = require('../logger'); const logger = require('../logger');
const db_api = require('../db'); const db_api = require('../db');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { uuid } = require('uuidv4'); const { uuid } = require('uuidv4');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const fs = require('fs-extra');
const path = require('path');
var LocalStrategy = require('passport-local').Strategy; var LocalStrategy = require('passport-local').Strategy;
var LdapStrategy = require('passport-ldapauth'); var LdapStrategy = require('passport-ldapauth');
@@ -16,7 +18,7 @@ var JwtStrategy = require('passport-jwt').Strategy,
let SERVER_SECRET = null; let SERVER_SECRET = null;
let JWT_EXPIRATION = null; let JWT_EXPIRATION = null;
let opts = null; let opts = null;
let saltRounds = null; let saltRounds = 10;
exports.initialize = function () { exports.initialize = function () {
/************************* /*************************
@@ -31,8 +33,6 @@ exports.initialize = function () {
}); });
} }
saltRounds = 10;
// Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813 // Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration'); JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
if (!(+JWT_EXPIRATION)) { if (!(+JWT_EXPIRATION)) {
@@ -68,14 +68,7 @@ exports.initialize = function () {
const setupRoles = async () => { const setupRoles = async () => {
const required_roles = { const required_roles = {
admin: { admin: {
permissions: [ permissions: CONSTS.AVAILABLE_PERMISSIONS
'filemanager',
'settings',
'subscriptions',
'sharing',
'advanced_download',
'downloads_manager'
]
}, },
user: { user: {
permissions: [ permissions: [
@@ -113,55 +106,41 @@ exports.passport.deserializeUser(function(user, done) {
/*************************************** /***************************************
* Register user with hashed password * Register user with hashed password
**************************************/ **************************************/
exports.registerUser = async function(req, res) {
var userid = req.body.userid;
var username = req.body.username;
var plaintextPassword = req.body.password;
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) { exports.registerUser = async (userid, username, plaintextPassword) => {
res.sendStatus(409); const hash = await bcrypt.hash(plaintextPassword, saltRounds);
logger.error(`Registration failed for user ${userid}. Registration is disabled.`); const new_user = generateUserObject(userid, username, hash);
return; // check if user exists
if (await db_api.getRecord('users', {uid: userid})) {
// user id is taken!
logger.error('Registration failed: UID is already taken!');
return null;
} else if (await db_api.getRecord('users', {name: username})) {
// user name is taken!
logger.error('Registration failed: User name is already taken!');
return null;
} else {
// add to db
await db_api.insertRecordIntoTable('users', new_user);
logger.verbose(`New user created: ${new_user.name}`);
return new_user;
} }
}
if (plaintextPassword === "") { exports.deleteUser = async (uid) => {
res.sendStatus(400); let success = false;
logger.error(`Registration failed for user ${userid}. A password must be provided.`); let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
return; const user_folder = path.join(__dirname, usersFileFolder, uid);
const user_db_obj = await db_api.getRecord('users', {uid: uid});
if (user_db_obj) {
// user exists, let's delete
await fs.remove(user_folder);
await db_api.removeRecord('users', {uid: uid});
success = true;
} else {
logger.error(`Could not find user with uid ${uid}`);
} }
return success;
bcrypt.hash(plaintextPassword, saltRounds)
.then(async function(hash) {
let new_user = generateUserObject(userid, username, hash);
// check if user exists
if (await db_api.getRecord('users', {uid: userid})) {
// user id is taken!
logger.error('Registration failed: UID is already taken!');
res.status(409).send('UID is already taken!');
} else if (await db_api.getRecord('users', {name: username})) {
// user name is taken!
logger.error('Registration failed: User name is already taken!');
res.status(409).send('User name is already taken!');
} else {
// add to db
await db_api.insertRecordIntoTable('users', new_user);
logger.verbose(`New user created: ${new_user.name}`);
res.send({
user: new_user
});
}
})
.then(function(result) {
})
.catch(function(err) {
logger.error(err);
if( err.code == 'ER_DUP_ENTRY' ) {
res.status(409).send('UserId already taken');
} else {
res.sendStatus(409);
}
});
} }
/*************************************** /***************************************
@@ -242,7 +221,7 @@ exports.returnAuthResponse = async function(req, res) {
user: req.user, user: req.user,
token: req.token, token: req.token,
permissions: await exports.userPermissions(req.user.uid), permissions: await exports.userPermissions(req.user.uid),
available_permissions: consts['AVAILABLE_PERMISSIONS'] available_permissions: CONSTS.AVAILABLE_PERMISSIONS
}); });
} }
@@ -326,7 +305,7 @@ exports.getUserVideos = async function(user_uid, type) {
} }
exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) { exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) {
let file = await db_api.getRecord('files', {file_uid: file_uid}); let file = await db_api.getRecord('files', {uid: file_uid});
// prevent unauthorized users from accessing the file info // prevent unauthorized users from accessing the file info
if (file && !file['sharingEnabled'] && requireSharing) file = null; if (file && !file['sharingEnabled'] && requireSharing) file = null;
@@ -413,8 +392,8 @@ exports.userPermissions = async function(user_uid) {
const role_obj = await db_api.getRecord('roles', {key: role}); const role_obj = await db_api.getRecord('roles', {key: role});
const role_permissions = role_obj['permissions']; const role_permissions = role_obj['permissions'];
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) { for (let i = 0; i < CONSTS.AVAILABLE_PERMISSIONS.length; i++) {
let permission = consts['AVAILABLE_PERMISSIONS'][i]; let permission = CONSTS.AVAILABLE_PERMISSIONS[i];
const user_has_explicit_permission = user_obj['permissions'].includes(permission); const user_has_explicit_permission = user_obj['permissions'].includes(permission);
const permission_in_overrides = user_obj['permission_overrides'].includes(permission); const permission_in_overrides = user_obj['permission_overrides'].includes(permission);

View File

@@ -347,6 +347,8 @@ const YTDL_ARGS_WITH_VALUES = [
'--convert-subs' '--convert-subs'
]; ];
exports.SUBSCRIPTION_BACKUP_PATH = 'subscription_backup.json'
// we're using a Set here for performance // we're using a Set here for performance
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES); exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
CMD="npm start" CMD="npm start && pm2 start"
# if the first arg starts with "-" pass it to program # if the first arg starts with "-" pass it to program
if [ "${1#-}" != "$1" ]; then if [ "${1#-}" != "$1" ]; then
@@ -10,7 +10,7 @@ fi
# chown current working directory to current user # chown current working directory to current user
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
find . \! -user "$UID" -exec chown "$UID:$GID" '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos." find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
exec gosu "$UID:$GID" "$0" "$@" exec gosu "$UID:$GID" "$0" "$@"
fi fi

View File

@@ -769,6 +769,11 @@
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
} }
}, },
"command-exists": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
"integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="
},
"compress-commons": { "compress-commons": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz",

View File

@@ -30,6 +30,7 @@
"async-mutex": "^0.4.0", "async-mutex": "^0.4.0",
"axios": "^0.21.2", "axios": "^0.21.2",
"bcryptjs": "^2.4.0", "bcryptjs": "^2.4.0",
"command-exists": "^1.2.9",
"compression": "^1.7.4", "compression": "^1.7.4",
"config": "^3.2.3", "config": "^3.2.3",
"express": "^4.18.2", "express": "^4.18.2",

View File

@@ -6,20 +6,21 @@ const config_api = require('./config');
const archive_api = require('./archive'); const archive_api = require('./archive');
const utils = require('./utils'); const utils = require('./utils');
const logger = require('./logger'); const logger = require('./logger');
const CONSTS = require('./consts');
const debugMode = process.env.YTDL_MODE === 'debug'; const debugMode = process.env.YTDL_MODE === 'debug';
const db_api = require('./db'); const db_api = require('./db');
const downloader_api = require('./downloader'); const downloader_api = require('./downloader');
async function subscribe(sub, user_uid = null) { exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => {
const result_obj = { const result_obj = {
success: false, success: false,
error: '' error: ''
}; };
return new Promise(async resolve => { return new Promise(async resolve => {
// sub should just have url and name. here we will get isPlaylist and path // sub should just have url and name. here we will get isPlaylist and path
sub.isPlaylist = sub.url.includes('playlist'); sub.isPlaylist = sub.isPlaylist || sub.url.includes('playlist');
sub.videos = []; sub.videos = [];
let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid})); let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid}));
@@ -34,10 +35,11 @@ async function subscribe(sub, user_uid = null) {
sub['user_uid'] = user_uid ? user_uid : undefined; sub['user_uid'] = user_uid ? user_uid : undefined;
await db_api.insertRecordIntoTable('subscriptions', sub); await db_api.insertRecordIntoTable('subscriptions', sub);
let success = await getSubscriptionInfo(sub); let success = skip_get_info ? true : await getSubscriptionInfo(sub);
exports.writeSubscriptionMetadata(sub);
if (success) { if (success) {
getVideosForSub(sub, user_uid); if (!sub.paused) exports.getVideosForSub(sub, user_uid);
} else { } else {
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.') logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
} }
@@ -109,7 +111,7 @@ async function getSubscriptionInfo(sub) {
}); });
} }
async function unsubscribe(sub, deleteMode, user_uid = null) { exports.unsubscribe = async (sub, deleteMode, user_uid = null) => {
let basePath = null; let basePath = null;
if (user_uid) if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions'); basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
@@ -148,7 +150,7 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
await db_api.removeAllRecords('archives', {sub_id: sub.id}); await db_api.removeAllRecords('archives', {sub_id: sub.id});
} }
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) { exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = null, user_uid = null) => {
if (typeof sub === 'string') { if (typeof sub === 'string') {
// TODO: fix bad workaround where sub is a sub_id // TODO: fix bad workaround where sub is a sub_id
sub = await db_api.getRecord('subscriptions', {sub_id: sub}); sub = await db_api.getRecord('subscriptions', {sub_id: sub});
@@ -216,8 +218,8 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
} }
} }
async function getVideosForSub(sub, user_uid = null) { exports.getVideosForSub = async (sub, user_uid = null) => {
const latest_sub_obj = await getSubscription(sub.id); const latest_sub_obj = await exports.getSubscription(sub.id);
if (!latest_sub_obj || latest_sub_obj['downloading']) { if (!latest_sub_obj || latest_sub_obj['downloading']) {
return false; return false;
} }
@@ -305,7 +307,7 @@ async function handleOutputJSON(output, sub, user_uid) {
} }
const files_to_download = await getFilesToDownload(sub, output_jsons); const files_to_download = await getFilesToDownload(sub, output_jsons);
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid); const base_download_options = exports.generateOptionsForSubscriptionDownload(sub, user_uid);
for (let j = 0; j < files_to_download.length; j++) { for (let j = 0; j < files_to_download.length; j++) {
const file_to_download = files_to_download[j]; const file_to_download = files_to_download[j];
@@ -316,7 +318,7 @@ async function handleOutputJSON(output, sub, user_uid) {
return files_to_download; return files_to_download;
} }
function generateOptionsForSubscriptionDownload(sub, user_uid) { exports.generateOptionsForSubscriptionDownload = (sub, user_uid) => {
let basePath = null; let basePath = null;
if (user_uid) if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions'); basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
@@ -440,35 +442,36 @@ async function getFilesToDownload(sub, output_jsons) {
} }
async function getSubscriptions(user_uid = null) { exports.getSubscriptions = async (user_uid = null) => {
return await db_api.getRecords('subscriptions', {user_uid: user_uid}); return await db_api.getRecords('subscriptions', {user_uid: user_uid});
} }
async function getAllSubscriptions() { exports.getAllSubscriptions = async () => {
const all_subs = await db_api.getRecords('subscriptions'); const all_subs = await db_api.getRecords('subscriptions');
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); 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) { exports.getSubscription = async (subID) => {
// stringify and parse because we may override the 'downloading' property // stringify and parse because we may override the 'downloading' property
const sub = JSON.parse(JSON.stringify(await db_api.getRecord('subscriptions', {id: subID}))); const sub = JSON.parse(JSON.stringify(await db_api.getRecord('subscriptions', {id: subID})));
// now with the download_queue, we may need to override 'downloading' // now with the download_queue, we may need to override 'downloading'
const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: sub.id}, true); const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: subID}, true);
if (!sub['downloading']) sub['downloading'] = current_downloads > 0; if (!sub['downloading']) sub['downloading'] = current_downloads > 0;
return sub; return sub;
} }
async function getSubscriptionByName(subName, user_uid = null) { exports.getSubscriptionByName = async (subName, user_uid = null) => {
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid}); return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
} }
async function updateSubscription(sub) { exports.updateSubscription = async (sub) => {
await db_api.updateRecord('subscriptions', {id: sub.id}, sub); await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
exports.writeSubscriptionMetadata(sub);
return true; return true;
} }
async function updateSubscriptionPropertyMultiple(subs, assignment_obj) { exports.updateSubscriptionPropertyMultiple = async (subs, assignment_obj) => {
subs.forEach(async sub => { subs.forEach(async sub => {
await updateSubscriptionProperty(sub, assignment_obj); await updateSubscriptionProperty(sub, assignment_obj);
}); });
@@ -480,6 +483,14 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
return true; return true;
} }
exports.writeSubscriptionMetadata = (sub) => {
let basePath = sub.user_uid ? path.join(config_api.getConfigItem('ytdl_users_base_path'), sub.user_uid, 'subscriptions')
: config_api.getConfigItem('ytdl_subscriptions_base_path');
const appendedBasePath = getAppendedBasePath(sub, basePath);
const metadata_path = path.join(appendedBasePath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
fs.writeJSONSync(metadata_path, sub);
}
async function setFreshUploads(sub) { async function setFreshUploads(sub) {
const sub_files = await db_api.getRecords('files', {sub_id: sub.id}); const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
if (!sub_files) return; if (!sub_files) return;
@@ -534,17 +545,3 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
function getAppendedBasePath(sub, base_path) { function getAppendedBasePath(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name); return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
} }
module.exports = {
getSubscription : getSubscription,
getSubscriptionByName : getSubscriptionByName,
getSubscriptions : getSubscriptions,
getAllSubscriptions : getAllSubscriptions,
updateSubscription : updateSubscription,
subscribe : subscribe,
unsubscribe : unsubscribe,
deleteSubscriptionFile : deleteSubscriptionFile,
getVideosForSub : getVideosForSub,
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
}

View File

@@ -3,10 +3,17 @@ const notifications_api = require('./notifications');
const youtubedl_api = require('./youtube-dl'); const youtubedl_api = require('./youtube-dl');
const archive_api = require('./archive'); const archive_api = require('./archive');
const files_api = require('./files'); const files_api = require('./files');
const subscriptions_api = require('./subscriptions');
const config_api = require('./config');
const auth_api = require('./authentication/auth');
const utils = require('./utils');
const logger = require('./logger');
const CONSTS = require('./consts');
const fs = require('fs-extra'); const fs = require('fs-extra');
const logger = require('./logger'); const path = require('path');
const scheduler = require('node-schedule'); const scheduler = require('node-schedule');
const { uuid } = require('uuidv4');
const TASKS = { const TASKS = {
backup_local_db: { backup_local_db: {
@@ -47,6 +54,11 @@ const TASKS = {
run: archive_api.importArchives, run: archive_api.importArchives,
title: 'Import legacy archives', title: 'Import legacy archives',
job: null job: null
},
rebuild_database: {
run: rebuildDB,
title: 'Rebuild database',
job: null
} }
} }
@@ -265,4 +277,68 @@ async function autoDeleteFiles(data) {
} }
} }
async function rebuildDB() {
await db_api.backupDB();
let subs_to_add = await guessSubscriptions(false);
subs_to_add = subs_to_add.concat(await guessSubscriptions(true));
const users_to_add = await guessUsers();
for (const user_to_add of users_to_add) {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const user_exists = await db_api.getRecord('users', {uid: user_to_add});
if (!user_exists) {
await auth_api.registerUser(user_to_add, user_to_add, 'password');
logger.info(`Regenerated user ${user_to_add}`);
}
const user_channel_subs = await guessSubscriptions(false, path.join(usersFileFolder, user_to_add), user_to_add);
const user_playlist_subs = await guessSubscriptions(true, path.join(usersFileFolder, user_to_add), user_to_add);
subs_to_add = subs_to_add.concat(user_channel_subs, user_playlist_subs);
}
for (const sub_to_add of subs_to_add) {
const sub_exists = !!(await subscriptions_api.getSubscriptionByName(sub_to_add['name'], sub_to_add['user_uid']));
// TODO: we shouldn't be creating this here
const new_sub = Object.assign({}, sub_to_add, {paused: true});
if (!sub_exists) {
await subscriptions_api.subscribe(new_sub, sub_to_add['user_uid'], true);
logger.info(`Regenerated subscription ${sub_to_add['name']}`);
}
}
logger.info(`Importing unregistered files`);
await files_api.importUnregisteredFiles();
}
const guessUsers = async () => {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const userPaths = await utils.getDirectoriesInDirectory(usersFileFolder);
return userPaths.map(userPath => path.basename(userPath));
}
const guessSubscriptions = async (isPlaylist, basePath = null) => {
const guessed_subs = [];
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
const subsSubPath = basePath ? path.join(basePath, 'subscriptions') : subscriptionsFileFolder;
const subsPath = path.join(subsSubPath, isPlaylist ? 'playlists' : 'channels');
const subs = await utils.getDirectoriesInDirectory(subsPath);
for (const subPath of subs) {
const sub_backup_path = path.join(subPath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
if (!fs.existsSync(sub_backup_path)) continue;
try {
const sub_backup = fs.readJSONSync(sub_backup_path)
delete sub_backup['_id'];
guessed_subs.push(sub_backup);
} catch(err) {
logger.warn(`Failed to reimport subscription in path ${subPath}`)
logger.warn(err);
}
}
return guessed_subs;
}
exports.TASKS = TASKS; exports.TASKS = TASKS;

View File

@@ -337,16 +337,22 @@ describe('Database', async function() {
}); });
describe('Multi User', async function() { describe('Multi User', async function() {
let user = null; const user_to_test = 'test_user';
const user_to_test = 'admin'; const user_password = 'test_pass';
const sub_to_test = 'dc834388-3454-41bf-a618-e11cb8c7de1c'; const sub_to_test = '';
const playlist_to_test = 'ysabVZz4x'; const playlist_to_test = '';
beforeEach(async function() { beforeEach(async function() {
await db_api.connectToDB(); await db_api.connectToDB();
user = await auth_api.login('admin', 'pass'); await auth_api.deleteUser(user_to_test);
}); });
describe('Authentication', function() { describe('Basic', function() {
it('login', async function() { it('Register', async function() {
const user = await auth_api.registerUser(user_to_test, user_to_test, user_password);
assert(user);
});
it('Login', async function() {
await auth_api.registerUser(user_to_test, user_to_test, user_password);
const user = await auth_api.login(user_to_test, user_password);
assert(user); assert(user);
}); });
}); });
@@ -362,14 +368,14 @@ describe('Multi User', async function() {
}); });
it('Video access - disallowed', async function() { it('Video access - disallowed', async function() {
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test); await db_api.setVideoProperty(video_to_test, {sharingEnabled: false});
const video_obj = auth_api.getUserVideo('admin', video_to_test, true); const video_obj = auth_api.getUserVideo(user_to_test, video_to_test, true);
assert(!video_obj); assert(!video_obj);
}); });
it('Video access - allowed', async function() { it('Video access - allowed', async function() {
await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test); await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test);
const video_obj = auth_api.getUserVideo('admin', video_to_test, true); const video_obj = auth_api.getUserVideo(user_to_test, video_to_test, true);
assert(video_obj); assert(video_obj);
}); });
}); });

View File

@@ -6,6 +6,7 @@ const fs = require('fs-extra')
const path = require('path'); const path = require('path');
const { promisify } = require('util'); const { promisify } = require('util');
const child_process = require('child_process'); const child_process = require('child_process');
const commandExistsSync = require('command-exists').sync;
async function getCommentsForVOD(vodId) { async function getCommentsForVOD(vodId) {
const exec = promisify(child_process.exec); const exec = promisify(child_process.exec);
@@ -20,7 +21,7 @@ async function getCommentsForVOD(vodId) {
const cliExt = is_windows ? '.exe' : '' const cliExt = is_windows ? '.exe' : ''
const cliPath = `TwitchDownloaderCLI${cliExt}` const cliPath = `TwitchDownloaderCLI${cliExt}`
if (!fs.existsSync(cliPath)) { if (!commandExistsSync(cliPath)) {
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`); logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
return null; return null;
} }

View File

@@ -519,6 +519,17 @@ exports.convertFlatObjectToNestedObject = (obj) => {
return result; return result;
} }
exports.getDirectoriesInDirectory = async (basePath) => {
try {
const files = await fs.readdir(basePath, { withFileTypes: true });
return files
.filter((file) => file.isDirectory())
.map((file) => path.join(basePath, file.name));
} catch (err) {
return [];
}
}
// objects // objects
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) { function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {

View File

@@ -0,0 +1,39 @@
#!/bin/sh
# THANK YOU TALULAH (https://github.com/nottalulah) for your help in figuring this out
# and also optimizing some code with this commit.
# xoxo :D
case $(uname -m) in
x86_64)
ARCH=Linux-x64;;
aarch64)
ARCH=LinuxArm64;;
armhf)
ARCH=LinuxArm;;
armv7)
ARCH=LinuxArm;;
armv7l)
ARCH=LinuxArm;;
*)
echo "Unsupported architecture: $(uname -m)"
exit 1
esac
echo "(INFO) Architecture detected: $ARCH"
echo "(1/5) READY - Install unzip"
apt-get update && apt-get -y install unzip curl jq libicu70
VERSION=$(curl --silent "https://api.github.com/repos/lay295/TwitchDownloader/releases" | jq -r --arg arch "$ARCH" '[.[] | select(.assets | length > 0) | select(.assets[].name | contains("CLI") and contains($arch))] | max_by(.published_at) | .tag_name')
echo "(2/5) DOWNLOAD - Acquire twitchdownloader"
curl -o twitchdownloader.zip \
--connect-timeout 5 \
--max-time 120 \
--retry 5 \
--retry-delay 0 \
--retry-max-time 40 \
-L "https://github.com/lay295/TwitchDownloader/releases/download/$VERSION/TwitchDownloaderCLI-$VERSION-$ARCH.zip"
unzip twitchdownloader.zip
chmod +x TwitchDownloaderCLI
echo "(3/5) Smoke test"
./TwitchDownloaderCLI --help
cp ./TwitchDownloaderCLI /usr/local/bin/TwitchDownloaderCLI

View File

@@ -30,7 +30,7 @@ curl -o ffmpeg.txz \
--retry 5 \ --retry 5 \
--retry-delay 0 \ --retry-delay 0 \
--retry-max-time 40 \ --retry-max-time 40 \
"https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-${ARCH}-static.tar.xz" "https://johnvansickle.com/ffmpeg/old-releases/ffmpeg-5.1.1-${ARCH}-static.tar.xz"
mkdir /tmp/ffmpeg mkdir /tmp/ffmpeg
tar xf ffmpeg.txz -C /tmp/ffmpeg tar xf ffmpeg.txz -C /tmp/ffmpeg
echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer" echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer"

View File

@@ -104,6 +104,7 @@ export type { SubscriptionRequestData } from './models/SubscriptionRequestData';
export type { SuccessObject } from './models/SuccessObject'; export type { SuccessObject } from './models/SuccessObject';
export type { TableInfo } from './models/TableInfo'; export type { TableInfo } from './models/TableInfo';
export type { Task } from './models/Task'; export type { Task } from './models/Task';
export { TaskType } from './models/TaskType';
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest'; export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse'; export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
export type { TransferDBRequest } from './models/TransferDBRequest'; export type { TransferDBRequest } from './models/TransferDBRequest';

View File

@@ -2,6 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { TaskType } from './TaskType';
export type GetTaskRequest = { export type GetTaskRequest = {
task_key: string; task_key: TaskType;
}; };

View File

@@ -2,8 +2,11 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Schedule } from './Schedule';
import type { TaskType } from './TaskType';
export type Task = { export type Task = {
key: string; key: TaskType;
title?: string; title?: string;
last_ran: number; last_ran: number;
last_confirmed: number; last_confirmed: number;
@@ -11,6 +14,6 @@ export type Task = {
confirming: boolean; confirming: boolean;
data: any; data: any;
error: string; error: string;
schedule: any; schedule: Schedule;
options?: any; options?: any;
}; };

View File

@@ -0,0 +1,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export enum TaskType {
BACKUP_LOCAL_DB = 'backup_local_db',
MISSING_FILES_CHECK = 'missing_files_check',
MISSING_DB_RECORDS = 'missing_db_records',
DUPLICATE_FILES_CHECK = 'duplicate_files_check',
YOUTUBEDL_UPDATE_CHECK = 'youtubedl_update_check',
DELETE_OLD_FILES = 'delete_old_files',
IMPORT_LEGACY_ARCHIVES = 'import_legacy_archives',
REBUILD_DATABASE = 'rebuild_database',
}

View File

@@ -2,7 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { TaskType } from './TaskType';
export type UpdateTaskDataRequest = { export type UpdateTaskDataRequest = {
task_key: string; task_key: TaskType;
new_data: any; new_data: any;
}; };

View File

@@ -2,7 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { TaskType } from './TaskType';
export type UpdateTaskOptionsRequest = { export type UpdateTaskOptionsRequest = {
task_key: string; task_key: TaskType;
new_options: any; new_options: any;
}; };

View File

@@ -3,8 +3,9 @@
/* eslint-disable */ /* eslint-disable */
import type { Schedule } from './Schedule'; import type { Schedule } from './Schedule';
import type { TaskType } from './TaskType';
export type UpdateTaskScheduleRequest = { export type UpdateTaskScheduleRequest = {
task_key: string; task_key: TaskType;
new_schedule: Schedule; new_schedule: Schedule;
}; };

View File

@@ -9,4 +9,5 @@ export enum UserPermission {
SHARING = 'sharing', SHARING = 'sharing',
ADVANCED_DOWNLOAD = 'advanced_download', ADVANCED_DOWNLOAD = 'advanced_download',
DOWNLOADS_MANAGER = 'downloads_manager', DOWNLOADS_MANAGER = 'downloads_manager',
TASKS_MANAGER = 'tasks_manager',
} }

View File

@@ -84,8 +84,8 @@ export class AppComponent implements OnInit, AfterViewInit {
this.postsService.open_create_default_admin_dialog.subscribe(open => { this.postsService.open_create_default_admin_dialog.subscribe(open => {
if (open) { if (open) {
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent); const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
dialogRef.afterClosed().subscribe(success => { dialogRef.afterClosed().subscribe(res => {
if (success) { if (!res || !res['user']) {
if (this.router.url !== '/login') { this.router.navigate(['/login']); } if (this.router.url !== '/login') { this.router.navigate(['/login']); }
} else { } else {
console.error('Failed to create default admin account. See logs for details.'); console.error('Failed to create default admin account. See logs for details.');

View File

@@ -1,6 +1,6 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Task } from 'api-types'; import { Task, TaskType } from 'api-types';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
@Component({ @Component({
@@ -9,7 +9,7 @@ import { PostsService } from 'app/posts.services';
styleUrls: ['./task-settings.component.scss'] styleUrls: ['./task-settings.component.scss']
}) })
export class TaskSettingsComponent { export class TaskSettingsComponent {
task_key: string; task_key: TaskType;
new_options = {}; new_options = {};
task: Task = null; task: Task = null;

View File

@@ -49,7 +49,7 @@
<mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell> <mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element"> <mat-cell *matCellDef="let element">
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-start">
<div *ngIf="element.data?.uids?.length > 0 || (!element.data?.uids && element.data)" class="col-12 mt-2" style="display: flex; justify-content: center;"> <div *ngIf="element.data?.uids?.length > 0 || (!element.data?.uids && element.data)" class="col-12 mt-2" style="display: flex; justify-content: center;">
<ng-container> <ng-container>
<button (click)="confirmTask(element.key)" [disabled]="element.running || element.confirming" mat-stroked-button> <button (click)="confirmTask(element.key)" [disabled]="element.running || element.confirming" mat-stroked-button>

View File

@@ -7,7 +7,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo
import { RestoreDbDialogComponent } from 'app/dialogs/restore-db-dialog/restore-db-dialog.component'; import { RestoreDbDialogComponent } from 'app/dialogs/restore-db-dialog/restore-db-dialog.component';
import { UpdateTaskScheduleDialogComponent } from 'app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component'; import { UpdateTaskScheduleDialogComponent } from 'app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { Task } from 'api-types'; import { Task, TaskType } from 'api-types';
import { TaskSettingsComponent } from '../task-settings/task-settings.component'; import { TaskSettingsComponent } from '../task-settings/task-settings.component';
import { Clipboard } from '@angular/cdk/clipboard'; import { Clipboard } from '@angular/cdk/clipboard';
@@ -28,6 +28,15 @@ export class TasksComponent implements OnInit {
db_backups = []; db_backups = [];
TASKS_TO_REQUIRE_DIALOG: { [key in TaskType]? : {dialogTitle: string, dialogText: string, submitText: string, warnSubmitColor: boolean}} = {
[TaskType.REBUILD_DATABASE]: {
dialogTitle: $localize`Rebuild database`,
dialogText: $localize`Are you sure you want to rebuild the database? All missing users, subscriptions, and files will be reimported. Note that if missing users are detected, they will be created with the password: 'password'. A backup of your current database will be created.`,
submitText: $localize`Rebuild database`,
warnSubmitColor: false
}
}
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@@ -79,7 +88,29 @@ export class TasksComponent implements OnInit {
}); });
} }
runTask(task_key: string): void { runTask(task_key: TaskType): void {
const taskToRequireDialog = this.TASKS_TO_REQUIRE_DIALOG[task_key];
if (taskToRequireDialog) {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
dialogTitle: taskToRequireDialog['dialogTitle'],
dialogText: taskToRequireDialog['dialogText'],
submitText: taskToRequireDialog['submitText'],
warnSubmitColor: taskToRequireDialog['warnSubmitColor']
}
});
dialogRef.afterClosed().subscribe(confirmed => {
if (confirmed) {
this._runTask(task_key);
}
});
return;
}
this._runTask(task_key);
}
_runTask(task_key: TaskType): void {
this.postsService.runTask(task_key).subscribe(res => { this.postsService.runTask(task_key).subscribe(res => {
this.getTasks(); this.getTasks();
this.getDBBackups(); this.getDBBackups();
@@ -91,7 +122,7 @@ export class TasksComponent implements OnInit {
}); });
} }
confirmTask(task_key: string): void { confirmTask(task_key: TaskType): void {
this.postsService.confirmTask(task_key).subscribe(res => { this.postsService.confirmTask(task_key).subscribe(res => {
this.getTasks(); this.getTasks();
if (res['success']) this.postsService.openSnackBar($localize`Successfully confirmed task!`); if (res['success']) this.postsService.openSnackBar($localize`Successfully confirmed task!`);

View File

@@ -112,7 +112,8 @@ import {
ImportArchiveRequest, ImportArchiveRequest,
Archive, Archive,
Subscription, Subscription,
RestartDownloadResponse RestartDownloadResponse,
TaskType
} from '../api-types'; } from '../api-types';
import { isoLangs } from './settings/locales_list'; import { isoLangs } from './settings/locales_list';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
@@ -640,32 +641,32 @@ export class PostsService implements CanActivate {
return this.http.post<SuccessObject>(this.path + 'resetTasks', {}, this.httpOptions); return this.http.post<SuccessObject>(this.path + 'resetTasks', {}, this.httpOptions);
} }
getTask(task_key: string) { getTask(task_key: TaskType) {
const body: GetTaskRequest = {task_key: task_key}; const body: GetTaskRequest = {task_key: task_key};
return this.http.post<GetTaskResponse>(this.path + 'getTask', body, this.httpOptions); return this.http.post<GetTaskResponse>(this.path + 'getTask', body, this.httpOptions);
} }
runTask(task_key: string) { runTask(task_key: TaskType) {
const body: GetTaskRequest = {task_key: task_key}; const body: GetTaskRequest = {task_key: task_key};
return this.http.post<SuccessObject>(this.path + 'runTask', body, this.httpOptions); return this.http.post<SuccessObject>(this.path + 'runTask', body, this.httpOptions);
} }
confirmTask(task_key: string) { confirmTask(task_key: TaskType) {
const body: GetTaskRequest = {task_key: task_key}; const body: GetTaskRequest = {task_key: task_key};
return this.http.post<SuccessObject>(this.path + 'confirmTask', body, this.httpOptions); return this.http.post<SuccessObject>(this.path + 'confirmTask', body, this.httpOptions);
} }
updateTaskSchedule(task_key: string, schedule: Schedule) { updateTaskSchedule(task_key: TaskType, schedule: Schedule) {
const body: UpdateTaskScheduleRequest = {task_key: task_key, new_schedule: schedule}; const body: UpdateTaskScheduleRequest = {task_key: task_key, new_schedule: schedule};
return this.http.post<SuccessObject>(this.path + 'updateTaskSchedule', body, this.httpOptions); return this.http.post<SuccessObject>(this.path + 'updateTaskSchedule', body, this.httpOptions);
} }
updateTaskData(task_key: string, data: any) { updateTaskData(task_key: TaskType, data: any) {
const body: UpdateTaskDataRequest = {task_key: task_key, new_data: data}; const body: UpdateTaskDataRequest = {task_key: task_key, new_data: data};
return this.http.post<SuccessObject>(this.path + 'updateTaskData', body, this.httpOptions); return this.http.post<SuccessObject>(this.path + 'updateTaskData', body, this.httpOptions);
} }
updateTaskOptions(task_key: string, options: any) { updateTaskOptions(task_key: TaskType, options: any) {
const body: UpdateTaskOptionsRequest = {task_key: task_key, new_options: options}; const body: UpdateTaskOptionsRequest = {task_key: task_key, new_options: options};
return this.http.post<SuccessObject>(this.path + 'updateTaskOptions', body, this.httpOptions); return this.http.post<SuccessObject>(this.path + 'updateTaskOptions', body, this.httpOptions);
} }
@@ -734,7 +735,7 @@ export class PostsService implements CanActivate {
this.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']); this.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']);
} }
}, err => { }, err => {
if (err.status === 401) { if (err === 'Unauthorized') {
this.sendToLogin(); this.sendToLogin();
this.token = null; this.token = null;
this.resetHttpParams(); this.resetHttpParams();

View File

@@ -443,7 +443,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context>
<context context-type="linenumber">56</context> <context context-type="linenumber">58</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
@@ -463,7 +463,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">611</context> <context context-type="linenumber">609</context>
</context-group> </context-group>
<note priority="1" from="description">Cancel</note> <note priority="1" from="description">Cancel</note>
</trans-unit> </trans-unit>
@@ -905,7 +905,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">80</context> <context context-type="linenumber">81</context>
</context-group> </context-group>
<note priority="1" from="description">Close</note> <note priority="1" from="description">Close</note>
</trans-unit> </trans-unit>
@@ -1082,7 +1082,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context>
<context context-type="linenumber">58</context> <context context-type="linenumber">60</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
@@ -1090,11 +1090,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">81</context> <context context-type="linenumber">82</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">608</context> <context context-type="linenumber">606</context>
</context-group> </context-group>
<note priority="1" from="description">save user edit action button tooltip</note> <note priority="1" from="description">save user edit action button tooltip</note>
</trans-unit> </trans-unit>
@@ -1296,44 +1296,44 @@
<source>Delete success!</source> <source>Delete success!</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context> <context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">301</context> <context context-type="linenumber">302</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8348223454028662277" datatype="html"> <trans-unit id="8348223454028662277" datatype="html">
<source>OK.</source> <source>OK.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context> <context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">301</context> <context context-type="linenumber">302</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context> <context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">304</context> <context context-type="linenumber">305</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context> <context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">307</context> <context context-type="linenumber">308</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7405156667148936748" datatype="html"> <trans-unit id="7405156667148936748" datatype="html">
<source>Delete failed!</source> <source>Delete failed!</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context> <context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">304</context> <context context-type="linenumber">305</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context> <context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">307</context> <context context-type="linenumber">308</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8937901770314883418" datatype="html"> <trans-unit id="8937901770314883418" datatype="html">
<source>Successfully deleted file: </source> <source>Successfully deleted file: </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context> <context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">321</context> <context context-type="linenumber">322</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context> <context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">328</context> <context context-type="linenumber">329</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ddc31f2885b1b33a7651963254b0c197f2a64086" datatype="html"> <trans-unit id="ddc31f2885b1b33a7651963254b0c197f2a64086" datatype="html">
@@ -1758,39 +1758,35 @@
<source>Delete and redownload</source> <source>Delete and redownload</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context> <context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">39</context> <context context-type="linenumber">38</context>
</context-group> </context-group>
<note priority="1" from="description">Delete and redownload subscription video button</note> <note priority="1" from="description">Delete and redownload subscription video button</note>
</trans-unit> </trans-unit>
<trans-unit id="e58f5716d6c08b6a841eb003c9f9774b5c5d34a9" datatype="html">
<source>Delete and don&apos;t download again</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">45</context>
</context-group>
<note priority="1" from="description">Delete forever subscription video button</note>
</trans-unit>
<trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html"> <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
<source>Delete</source> <source>Delete</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context> <context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">44</context> <context context-type="linenumber">39</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context> <context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">45</context>
</context-group> </context-group>
<note priority="1" from="description">Delete video button</note> <note priority="1" from="description">Delete video button</note>
</trans-unit> </trans-unit>
<trans-unit id="e58f5716d6c08b6a841eb003c9f9774b5c5d34a9" datatype="html">
<source>Delete and don&apos;t download again</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
<note priority="1" from="description">Delete and don&apos;t download again</note>
</trans-unit>
<trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html"> <trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
<source>Edit</source> <source>Edit</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context> <context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">48</context> <context context-type="linenumber">43</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context> <context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
@@ -1965,11 +1961,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">316</context> <context context-type="linenumber">300</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">322</context> <context context-type="linenumber">306</context>
</context-group> </context-group>
<note priority="1" from="description">About bug click here</note> <note priority="1" from="description">About bug click here</note>
</trans-unit> </trans-unit>
@@ -2127,7 +2123,7 @@
<source>Add new rule</source> <source>Add new rule</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context>
<context context-type="linenumber">40</context> <context context-type="linenumber">42</context>
</context-group> </context-group>
<note priority="1" from="description">Add new rule tooltip</note> <note priority="1" from="description">Add new rule tooltip</note>
</trans-unit> </trans-unit>
@@ -2135,7 +2131,7 @@
<source>Custom file output</source> <source>Custom file output</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">47</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
@@ -2151,7 +2147,7 @@
<source>Documentation</source> <source>Documentation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context>
<context context-type="linenumber">49</context> <context context-type="linenumber">51</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
@@ -2171,7 +2167,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">381</context> <context context-type="linenumber">365</context>
</context-group> </context-group>
<note priority="1" from="description">Custom output template documentation link</note> <note priority="1" from="description">Custom output template documentation link</note>
</trans-unit> </trans-unit>
@@ -2179,7 +2175,7 @@
<source>Path is relative to the config download path. Don&apos;t include extension.</source> <source>Path is relative to the config download path. Don&apos;t include extension.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
@@ -2319,7 +2315,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">306</context> <context context-type="linenumber">290</context>
</context-group> </context-group>
<note priority="1" from="description">Generate RSS URL</note> <note priority="1" from="description">Generate RSS URL</note>
</trans-unit> </trans-unit>
@@ -2803,7 +2799,7 @@
<source>View count</source> <source>View count</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">51</context>
</context-group> </context-group>
<note priority="1" from="description">View count</note> <note priority="1" from="description">View count</note>
</trans-unit> </trans-unit>
@@ -2811,7 +2807,7 @@
<source>Local view count</source> <source>Local view count</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">55</context>
</context-group> </context-group>
<note priority="1" from="description">Local view count</note> <note priority="1" from="description">Local view count</note>
</trans-unit> </trans-unit>
@@ -2819,7 +2815,7 @@
<source>Resolution:</source> <source>Resolution:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">61</context> <context context-type="linenumber">62</context>
</context-group> </context-group>
<note priority="1" from="description">Video resolution property</note> <note priority="1" from="description">Video resolution property</note>
</trans-unit> </trans-unit>
@@ -2827,7 +2823,7 @@
<source>Audio bitrate:</source> <source>Audio bitrate:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">65</context> <context context-type="linenumber">66</context>
</context-group> </context-group>
<note priority="1" from="description">Video audio bitrate property</note> <note priority="1" from="description">Video audio bitrate property</note>
</trans-unit> </trans-unit>
@@ -2835,7 +2831,7 @@
<source>File size:</source> <source>File size:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">69</context> <context context-type="linenumber">70</context>
</context-group> </context-group>
<note priority="1" from="description">Video file size property</note> <note priority="1" from="description">Video file size property</note>
</trans-unit> </trans-unit>
@@ -2843,7 +2839,7 @@
<source>Path:</source> <source>Path:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context> <context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">73</context> <context context-type="linenumber">74</context>
</context-group> </context-group>
<note priority="1" from="description">Video path property</note> <note priority="1" from="description">Video path property</note>
</trans-unit> </trans-unit>
@@ -3011,18 +3007,18 @@
<source>Download failed!</source> <source>Download failed!</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context> <context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">382</context> <context context-type="linenumber">380</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context> <context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">788</context> <context context-type="linenumber">783</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7220285196408439810" datatype="html"> <trans-unit id="7220285196408439810" datatype="html">
<source>Download for <x id="url" equiv-text="url"/> has been queued!</source> <source>Download for <x id="url" equiv-text="url"/> has been queued!</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context> <context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">386</context> <context context-type="linenumber">384</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="dad95154dcef3509b8cc705046061fd24994bbb7" datatype="html"> <trans-unit id="dad95154dcef3509b8cc705046061fd24994bbb7" datatype="html">
@@ -3475,51 +3471,19 @@
</context-group> </context-group>
<note priority="1" from="description">Youtube API Key setting hint</note> <note priority="1" from="description">Youtube API Key setting hint</note>
</trans-unit> </trans-unit>
<trans-unit id="d162f9fcd6a7187b391e004f072ab3da8377c47d" datatype="html">
<source>Use Twitch API</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">273</context>
</context-group>
<note priority="1" from="description">Use Twitch API setting</note>
</trans-unit>
<trans-unit id="5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9" datatype="html"> <trans-unit id="5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9" datatype="html">
<source>Auto-download Twitch Chat</source> <source>Auto-download Twitch Chat</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">276</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<note priority="1" from="description">Auto download Twitch Chat setting</note> <note priority="1" from="description">Auto download Twitch Chat setting</note>
</trans-unit> </trans-unit>
<trans-unit id="5d78fe9ba69a8710613d3f7c35b22e9c8226e4dc" datatype="html">
<source>Twitch Client ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">280</context>
</context-group>
<note priority="1" from="description">Twitch Client ID</note>
</trans-unit>
<trans-unit id="4c9a15ab7fb3dce1002ea7aea4ecada3c1ee12e9" datatype="html">
<source>Generating an ID/secret is easy!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">282</context>
</context-group>
<note priority="1" from="description">Twitch Client ID setting hint</note>
</trans-unit>
<trans-unit id="8506540da14d205ea092b4c856e242ed7f500643" datatype="html">
<source>Twitch Client Secret</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">287</context>
</context-group>
<note priority="1" from="description">Twitch Client Secret</note>
</trans-unit>
<trans-unit id="c55604d30653e3d8310190d8d26761226132a901" datatype="html"> <trans-unit id="c55604d30653e3d8310190d8d26761226132a901" datatype="html">
<source>Enables a button to skip ads when viewing supported videos.</source> <source>Enables a button to skip ads when viewing supported videos.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">292</context> <context context-type="linenumber">276</context>
</context-group> </context-group>
<note priority="1" from="description">SponsorBlock API tooltip</note> <note priority="1" from="description">SponsorBlock API tooltip</note>
</trans-unit> </trans-unit>
@@ -3527,7 +3491,7 @@
<source>Use SponsorBlock API</source> <source>Use SponsorBlock API</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">292</context> <context context-type="linenumber">276</context>
</context-group> </context-group>
<note priority="1" from="description">Use SponsorBlock API setting</note> <note priority="1" from="description">Use SponsorBlock API setting</note>
</trans-unit> </trans-unit>
@@ -3535,7 +3499,7 @@
<source>Generates NFO files with every download, primarily used by Kodi.</source> <source>Generates NFO files with every download, primarily used by Kodi.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">295</context> <context context-type="linenumber">279</context>
</context-group> </context-group>
<note priority="1" from="description">Generate NFO files tooltip</note> <note priority="1" from="description">Generate NFO files tooltip</note>
</trans-unit> </trans-unit>
@@ -3543,7 +3507,7 @@
<source>Generate NFO files</source> <source>Generate NFO files</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">295</context> <context context-type="linenumber">279</context>
</context-group> </context-group>
<note priority="1" from="description">Generate NFO files setting</note> <note priority="1" from="description">Generate NFO files setting</note>
</trans-unit> </trans-unit>
@@ -3551,7 +3515,7 @@
<source>Enable RSS Feed</source> <source>Enable RSS Feed</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">304</context> <context context-type="linenumber">288</context>
</context-group> </context-group>
<note priority="1" from="description">Enable RSS Feed setting</note> <note priority="1" from="description">Enable RSS Feed setting</note>
</trans-unit> </trans-unit>
@@ -3559,7 +3523,7 @@
<source>Be careful enabling this with multi-user mode! User data may be exposed.</source> <source>Be careful enabling this with multi-user mode! User data may be exposed.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">305</context> <context context-type="linenumber">289</context>
</context-group> </context-group>
<note priority="1" from="description">RSS Feed prefix</note> <note priority="1" from="description">RSS Feed prefix</note>
</trans-unit> </trans-unit>
@@ -3567,7 +3531,7 @@
<source>See documentation here.</source> <source>See documentation here.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">307</context> <context context-type="linenumber">291</context>
</context-group> </context-group>
<note priority="1" from="description">RSS feed documentation</note> <note priority="1" from="description">RSS feed documentation</note>
</trans-unit> </trans-unit>
@@ -3575,7 +3539,7 @@
<source>to download the official YoutubeDL-Material Chrome extension manually.</source> <source>to download the official YoutubeDL-Material Chrome extension manually.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">316</context> <context context-type="linenumber">300</context>
</context-group> </context-group>
<note priority="1" from="description">Chrome click here suffix</note> <note priority="1" from="description">Chrome click here suffix</note>
</trans-unit> </trans-unit>
@@ -3583,7 +3547,7 @@
<source>You must manually load the extension and modify the extension&apos;s settings to set the frontend URL.</source> <source>You must manually load the extension and modify the extension&apos;s settings to set the frontend URL.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">317</context> <context context-type="linenumber">301</context>
</context-group> </context-group>
<note priority="1" from="description">Chrome setup suffix</note> <note priority="1" from="description">Chrome setup suffix</note>
</trans-unit> </trans-unit>
@@ -3591,7 +3555,7 @@
<source>to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page.</source> <source>to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">322</context> <context context-type="linenumber">306</context>
</context-group> </context-group>
<note priority="1" from="description">Firefox click here suffix</note> <note priority="1" from="description">Firefox click here suffix</note>
</trans-unit> </trans-unit>
@@ -3599,7 +3563,7 @@
<source>Detailed setup instructions.</source> <source>Detailed setup instructions.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">323</context> <context context-type="linenumber">307</context>
</context-group> </context-group>
<note priority="1" from="description">Firefox setup prefix link</note> <note priority="1" from="description">Firefox setup prefix link</note>
</trans-unit> </trans-unit>
@@ -3607,7 +3571,7 @@
<source>Not much is required other than changing the extension&apos;s settings to set the frontend URL.</source> <source>Not much is required other than changing the extension&apos;s settings to set the frontend URL.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">323</context> <context context-type="linenumber">307</context>
</context-group> </context-group>
<note priority="1" from="description">Firefox setup suffix</note> <note priority="1" from="description">Firefox setup suffix</note>
</trans-unit> </trans-unit>
@@ -3615,7 +3579,7 @@
<source>Drag the link below to your bookmarks, and you&apos;re good to go! Just navigate to the YouTube video you&apos;d like to download, and click the bookmark.</source> <source>Drag the link below to your bookmarks, and you&apos;re good to go! Just navigate to the YouTube video you&apos;d like to download, and click the bookmark.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">328</context> <context context-type="linenumber">312</context>
</context-group> </context-group>
<note priority="1" from="description">Bookmarklet instructions</note> <note priority="1" from="description">Bookmarklet instructions</note>
</trans-unit> </trans-unit>
@@ -3623,7 +3587,7 @@
<source>Generate &apos;audio only&apos; bookmarklet</source> <source>Generate &apos;audio only&apos; bookmarklet</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">329</context> <context context-type="linenumber">313</context>
</context-group> </context-group>
<note priority="1" from="description">Generate audio only bookmarklet checkbox</note> <note priority="1" from="description">Generate audio only bookmarklet checkbox</note>
</trans-unit> </trans-unit>
@@ -3631,7 +3595,7 @@
<source>Database</source> <source>Database</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">338</context> <context context-type="linenumber">322</context>
</context-group> </context-group>
<note priority="1" from="description">Database settings label</note> <note priority="1" from="description">Database settings label</note>
</trans-unit> </trans-unit>
@@ -3639,7 +3603,7 @@
<source>Database location:</source> <source>Database location:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">344</context> <context context-type="linenumber">328</context>
</context-group> </context-group>
<note priority="1" from="description">Database location label</note> <note priority="1" from="description">Database location label</note>
</trans-unit> </trans-unit>
@@ -3647,7 +3611,7 @@
<source>Records per table</source> <source>Records per table</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">345</context> <context context-type="linenumber">329</context>
</context-group> </context-group>
<note priority="1" from="description">Records per table label</note> <note priority="1" from="description">Records per table label</note>
</trans-unit> </trans-unit>
@@ -3655,7 +3619,7 @@
<source>MongoDB Connection String</source> <source>MongoDB Connection String</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">353</context> <context context-type="linenumber">337</context>
</context-group> </context-group>
<note priority="1" from="description">MongoDB Connection String</note> <note priority="1" from="description">MongoDB Connection String</note>
</trans-unit> </trans-unit>
@@ -3663,7 +3627,7 @@
<source>Example:</source> <source>Example:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">355</context> <context context-type="linenumber">339</context>
</context-group> </context-group>
<note priority="1" from="description">MongoDB Connection String setting hint AKA preamble</note> <note priority="1" from="description">MongoDB Connection String setting hint AKA preamble</note>
</trans-unit> </trans-unit>
@@ -3671,7 +3635,7 @@
<source>Test connection string</source> <source>Test connection string</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">359</context> <context context-type="linenumber">343</context>
</context-group> </context-group>
<note priority="1" from="description">Test connection string button</note> <note priority="1" from="description">Test connection string button</note>
</trans-unit> </trans-unit>
@@ -3679,7 +3643,7 @@
<source>Transfer DB to </source> <source>Transfer DB to </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">364</context> <context context-type="linenumber">348</context>
</context-group> </context-group>
<note priority="1" from="description">Transfer DB button</note> <note priority="1" from="description">Transfer DB button</note>
</trans-unit> </trans-unit>
@@ -3687,7 +3651,7 @@
<source>Database information could not be retrieved. Check the server logs for more information.</source> <source>Database information could not be retrieved. Check the server logs for more information.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">368</context> <context context-type="linenumber">352</context>
</context-group> </context-group>
<note priority="1" from="description">Database info not retrieved error message</note> <note priority="1" from="description">Database info not retrieved error message</note>
</trans-unit> </trans-unit>
@@ -3695,7 +3659,7 @@
<source>Notifications</source> <source>Notifications</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">376</context> <context context-type="linenumber">360</context>
</context-group> </context-group>
<note priority="1" from="description">Notifications settings label</note> <note priority="1" from="description">Notifications settings label</note>
</trans-unit> </trans-unit>
@@ -3703,7 +3667,7 @@
<source>Enable notifications</source> <source>Enable notifications</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">382</context> <context context-type="linenumber">366</context>
</context-group> </context-group>
<note priority="1" from="description">Enable notifications setting</note> <note priority="1" from="description">Enable notifications setting</note>
</trans-unit> </trans-unit>
@@ -3711,7 +3675,7 @@
<source>Enable all notifications</source> <source>Enable all notifications</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">385</context> <context context-type="linenumber">369</context>
</context-group> </context-group>
<note priority="1" from="description">Enable all notifications setting</note> <note priority="1" from="description">Enable all notifications setting</note>
</trans-unit> </trans-unit>
@@ -3719,7 +3683,7 @@
<source>Allowed notification types</source> <source>Allowed notification types</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">389</context> <context context-type="linenumber">373</context>
</context-group> </context-group>
<note priority="1" from="description">Allowed notification types</note> <note priority="1" from="description">Allowed notification types</note>
</trans-unit> </trans-unit>
@@ -3727,7 +3691,7 @@
<source>Download complete</source> <source>Download complete</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">391</context> <context context-type="linenumber">375</context>
</context-group> </context-group>
<note priority="1" from="description">Download complete</note> <note priority="1" from="description">Download complete</note>
</trans-unit> </trans-unit>
@@ -3735,7 +3699,7 @@
<source>Download error</source> <source>Download error</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">392</context> <context context-type="linenumber">376</context>
</context-group> </context-group>
<note priority="1" from="description">Download error</note> <note priority="1" from="description">Download error</note>
</trans-unit> </trans-unit>
@@ -3743,7 +3707,7 @@
<source>Task finished</source> <source>Task finished</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">393</context> <context context-type="linenumber">377</context>
</context-group> </context-group>
<note priority="1" from="description">Task finished</note> <note priority="1" from="description">Task finished</note>
</trans-unit> </trans-unit>
@@ -3751,15 +3715,55 @@
<source>Webhook URL</source> <source>Webhook URL</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">399</context> <context context-type="linenumber">383</context>
</context-group> </context-group>
<note priority="1" from="description">webhook URL</note> <note priority="1" from="description">webhook URL</note>
</trans-unit> </trans-unit>
<trans-unit id="3264d82792954815be755b3da01e2625458711dc" datatype="html">
<source>Discord Webhook URL</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">390</context>
</context-group>
<note priority="1" from="description">Discord Webhook URL</note>
</trans-unit>
<trans-unit id="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html">
<source>See docs here.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">392</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">399</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">409</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">419</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">426</context>
</context-group>
<note priority="1" from="description">Discord API setting hint</note>
</trans-unit>
<trans-unit id="7cedb649779673568447b994463b2882c4e0436a" datatype="html">
<source>Slack Webhook URL</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">397</context>
</context-group>
<note priority="1" from="description">Slack Webhook URL</note>
</trans-unit>
<trans-unit id="8c1bf02206fbc371ff69ab1b7e35a17ba29d169d" datatype="html"> <trans-unit id="8c1bf02206fbc371ff69ab1b7e35a17ba29d169d" datatype="html">
<source>Use ntfy API</source> <source>Use ntfy API</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">405</context> <context context-type="linenumber">403</context>
</context-group> </context-group>
<note priority="1" from="description">Use ntfy API setting</note> <note priority="1" from="description">Use ntfy API setting</note>
</trans-unit> </trans-unit>
@@ -3767,31 +3771,15 @@
<source>ntfy topic URL</source> <source>ntfy topic URL</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">409</context> <context context-type="linenumber">407</context>
</context-group> </context-group>
<note priority="1" from="description">ntfy topic URL</note> <note priority="1" from="description">ntfy topic URL</note>
</trans-unit> </trans-unit>
<trans-unit id="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html">
<source>See docs here.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">411</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">421</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">428</context>
</context-group>
<note priority="1" from="description">ntfy API setting hint</note>
</trans-unit>
<trans-unit id="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html"> <trans-unit id="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html">
<source>Use gotify API</source> <source>Use gotify API</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">415</context> <context context-type="linenumber">413</context>
</context-group> </context-group>
<note priority="1" from="description">Use gotify API setting</note> <note priority="1" from="description">Use gotify API setting</note>
</trans-unit> </trans-unit>
@@ -3799,7 +3787,7 @@
<source>Gotify server URL</source> <source>Gotify server URL</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">419</context> <context context-type="linenumber">417</context>
</context-group> </context-group>
<note priority="1" from="description">Gotify server URL</note> <note priority="1" from="description">Gotify server URL</note>
</trans-unit> </trans-unit>
@@ -3807,7 +3795,7 @@
<source>Gotify app token</source> <source>Gotify app token</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">426</context> <context context-type="linenumber">424</context>
</context-group> </context-group>
<note priority="1" from="description">Gotify app token</note> <note priority="1" from="description">Gotify app token</note>
</trans-unit> </trans-unit>
@@ -3815,7 +3803,7 @@
<source>Use Telegram API</source> <source>Use Telegram API</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">432</context> <context context-type="linenumber">430</context>
</context-group> </context-group>
<note priority="1" from="description">Use Telegram API setting</note> <note priority="1" from="description">Use Telegram API setting</note>
</trans-unit> </trans-unit>
@@ -3823,7 +3811,7 @@
<source>Telegram bot token</source> <source>Telegram bot token</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">436</context> <context context-type="linenumber">434</context>
</context-group> </context-group>
<note priority="1" from="description">Telegram bot token</note> <note priority="1" from="description">Telegram bot token</note>
</trans-unit> </trans-unit>
@@ -3831,7 +3819,7 @@
<source>Create bot here.</source> <source>Create bot here.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">438</context> <context context-type="linenumber">436</context>
</context-group> </context-group>
<note priority="1" from="description">Telegram bot create link</note> <note priority="1" from="description">Telegram bot create link</note>
</trans-unit> </trans-unit>
@@ -3839,7 +3827,7 @@
<source>Telegram chat ID</source> <source>Telegram chat ID</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">443</context> <context context-type="linenumber">441</context>
</context-group> </context-group>
<note priority="1" from="description">Telegram chat ID</note> <note priority="1" from="description">Telegram chat ID</note>
</trans-unit> </trans-unit>
@@ -3847,7 +3835,7 @@
<source>How do I get the chat ID?</source> <source>How do I get the chat ID?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">445</context> <context context-type="linenumber">443</context>
</context-group> </context-group>
<note priority="1" from="description">Telegram chat ID help</note> <note priority="1" from="description">Telegram chat ID help</note>
</trans-unit> </trans-unit>
@@ -3855,7 +3843,7 @@
<source>Advanced</source> <source>Advanced</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">453</context> <context context-type="linenumber">451</context>
</context-group> </context-group>
<note priority="1" from="description">Host settings label</note> <note priority="1" from="description">Host settings label</note>
</trans-unit> </trans-unit>
@@ -3863,7 +3851,7 @@
<source>Select a downloader</source> <source>Select a downloader</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">459</context> <context context-type="linenumber">457</context>
</context-group> </context-group>
<note priority="1" from="description">Default downloader select label</note> <note priority="1" from="description">Default downloader select label</note>
</trans-unit> </trans-unit>
@@ -3871,7 +3859,7 @@
<source>Restart required.</source> <source>Restart required.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">465</context> <context context-type="linenumber">463</context>
</context-group> </context-group>
<note priority="1" from="description">Restart required hint</note> <note priority="1" from="description">Restart required hint</note>
</trans-unit> </trans-unit>
@@ -3879,7 +3867,7 @@
<source>Use default downloading agent</source> <source>Use default downloading agent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">469</context> <context context-type="linenumber">467</context>
</context-group> </context-group>
<note priority="1" from="description">Use default downloading agent setting</note> <note priority="1" from="description">Use default downloading agent setting</note>
</trans-unit> </trans-unit>
@@ -3887,7 +3875,7 @@
<source>Select a download agent</source> <source>Select a download agent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">473</context> <context context-type="linenumber">471</context>
</context-group> </context-group>
<note priority="1" from="description">Custom downloader select label</note> <note priority="1" from="description">Custom downloader select label</note>
</trans-unit> </trans-unit>
@@ -3895,7 +3883,7 @@
<source>Log Level</source> <source>Log Level</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">487</context> <context context-type="linenumber">485</context>
</context-group> </context-group>
<note priority="1" from="description">Log Level label</note> <note priority="1" from="description">Log Level label</note>
</trans-unit> </trans-unit>
@@ -3903,7 +3891,7 @@
<source>Login expiration</source> <source>Login expiration</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">499</context> <context context-type="linenumber">497</context>
</context-group> </context-group>
<note priority="1" from="description">Login expiration select label</note> <note priority="1" from="description">Login expiration select label</note>
</trans-unit> </trans-unit>
@@ -3911,7 +3899,7 @@
<source>Allow advanced download</source> <source>Allow advanced download</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">510</context> <context context-type="linenumber">508</context>
</context-group> </context-group>
<note priority="1" from="description">Allow advanced downloading setting</note> <note priority="1" from="description">Allow advanced downloading setting</note>
</trans-unit> </trans-unit>
@@ -3919,7 +3907,7 @@
<source>Use Cookies</source> <source>Use Cookies</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">518</context> <context context-type="linenumber">516</context>
</context-group> </context-group>
<note priority="1" from="description">Use cookies setting</note> <note priority="1" from="description">Use cookies setting</note>
</trans-unit> </trans-unit>
@@ -3927,7 +3915,7 @@
<source>Set Cookies</source> <source>Set Cookies</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">519</context> <context context-type="linenumber">517</context>
</context-group> </context-group>
<note priority="1" from="description">Set cookies button</note> <note priority="1" from="description">Set cookies button</note>
</trans-unit> </trans-unit>
@@ -3935,7 +3923,7 @@
<source>Restart server</source> <source>Restart server</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">531</context> <context context-type="linenumber">529</context>
</context-group> </context-group>
<note priority="1" from="description">Restart server button</note> <note priority="1" from="description">Restart server button</note>
</trans-unit> </trans-unit>
@@ -3943,7 +3931,7 @@
<source>Users</source> <source>Users</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">540</context> <context context-type="linenumber">538</context>
</context-group> </context-group>
<note priority="1" from="description">Users settings label</note> <note priority="1" from="description">Users settings label</note>
</trans-unit> </trans-unit>
@@ -3951,7 +3939,7 @@
<source>Allow user registration</source> <source>Allow user registration</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">546</context> <context context-type="linenumber">544</context>
</context-group> </context-group>
<note priority="1" from="description">Allow registration setting</note> <note priority="1" from="description">Allow registration setting</note>
</trans-unit> </trans-unit>
@@ -3959,7 +3947,7 @@
<source>Auth method</source> <source>Auth method</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">550</context> <context context-type="linenumber">548</context>
</context-group> </context-group>
<note priority="1" from="description">Auth method</note> <note priority="1" from="description">Auth method</note>
</trans-unit> </trans-unit>
@@ -3967,7 +3955,7 @@
<source>Internal</source> <source>Internal</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">553</context> <context context-type="linenumber">551</context>
</context-group> </context-group>
<note priority="1" from="description">Internal auth method</note> <note priority="1" from="description">Internal auth method</note>
</trans-unit> </trans-unit>
@@ -3975,7 +3963,7 @@
<source>LDAP</source> <source>LDAP</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">556</context> <context context-type="linenumber">554</context>
</context-group> </context-group>
<note priority="1" from="description">LDAP auth method</note> <note priority="1" from="description">LDAP auth method</note>
</trans-unit> </trans-unit>
@@ -3983,7 +3971,7 @@
<source>LDAP URL</source> <source>LDAP URL</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">563</context> <context context-type="linenumber">561</context>
</context-group> </context-group>
<note priority="1" from="description">LDAP URL</note> <note priority="1" from="description">LDAP URL</note>
</trans-unit> </trans-unit>
@@ -3991,7 +3979,7 @@
<source>Bind DN</source> <source>Bind DN</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">569</context> <context context-type="linenumber">567</context>
</context-group> </context-group>
<note priority="1" from="description">Bind DN</note> <note priority="1" from="description">Bind DN</note>
</trans-unit> </trans-unit>
@@ -3999,7 +3987,7 @@
<source>Bind Credentials</source> <source>Bind Credentials</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">575</context> <context context-type="linenumber">573</context>
</context-group> </context-group>
<note priority="1" from="description">Bind Credentials</note> <note priority="1" from="description">Bind Credentials</note>
</trans-unit> </trans-unit>
@@ -4007,7 +3995,7 @@
<source>Search Base</source> <source>Search Base</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">581</context> <context context-type="linenumber">579</context>
</context-group> </context-group>
<note priority="1" from="description">Search Base</note> <note priority="1" from="description">Search Base</note>
</trans-unit> </trans-unit>
@@ -4015,7 +4003,7 @@
<source>Search Filter</source> <source>Search Filter</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">587</context> <context context-type="linenumber">585</context>
</context-group> </context-group>
<note priority="1" from="description">Search Filter</note> <note priority="1" from="description">Search Filter</note>
</trans-unit> </trans-unit>
@@ -4023,7 +4011,7 @@
<source>Logs</source> <source>Logs</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context> <context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">597</context> <context context-type="linenumber">595</context>
</context-group> </context-group>
<note priority="1" from="description">Logs settings label</note> <note priority="1" from="description">Logs settings label</note>
</trans-unit> </trans-unit>

View File

@@ -2577,7 +2577,7 @@
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2560364143605631750" datatype="html"> <trans-unit id="2560364143605631750" datatype="html">
<source>Error for<x id="url" equiv-text="download['url']"/></source> <source>Error for <x id="url" equiv-text="download['url']"/></source>
<target state="translated">Error para <x id="url" equiv-text="download['url']"/></target> <target state="translated">Error para <x id="url" equiv-text="download['url']"/></target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context> <context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
@@ -2733,8 +2733,8 @@
<note priority="1" from="description">Clear missing files from DB</note> <note priority="1" from="description">Clear missing files from DB</note>
</trans-unit> </trans-unit>
<trans-unit id="39921032161993566" datatype="html"> <trans-unit id="39921032161993566" datatype="html">
<source>Playlist created.</source> <source>Successfully created playlist!</source>
<target state="translated">Lista de reproducción creada.</target> <target state="translated">¡Lista de reproducción creada con éxito!</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context> <context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">56</context> <context context-type="linenumber">56</context>
@@ -3092,6 +3092,848 @@
</context-group> </context-group>
<note priority="1" from="description">Generate NFO files setting</note> <note priority="1" from="description">Generate NFO files setting</note>
</trans-unit> </trans-unit>
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html">
<source>Archives</source>
<target state="translated">Archivos</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Archives menu label</note>
</trans-unit>
<trans-unit id="5ca707824ab93066c7d9b44e1b8bf216725c2c22" datatype="html">
<source>Filter</source>
<target state="translated">Filtros</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
<note priority="1" from="description">Filter</note>
</trans-unit>
<trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
<source>ID</source>
<target state="translated">ID</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
<note priority="1" from="description">ID</note>
</trans-unit>
<trans-unit id="c150a30bbbdb175b4d08820196a9acb66b167653" datatype="html">
<source>Archives empty</source>
<target state="translated">Archivos vacíos</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">72</context>
</context-group>
<note priority="1" from="description">Archives empty</note>
</trans-unit>
<trans-unit id="51a161ce175abcd44f6c6cbd0e996681bf553ac3" datatype="html">
<source>Delete selected</source>
<target state="translated">Eliminar seleccionado</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">77</context>
</context-group>
<note priority="1" from="description">Delete selected</note>
</trans-unit>
<trans-unit id="c41475a25c9f9d9639db9efa56637603a77528b4" datatype="html">
<source>Download archive</source>
<target state="translated">Descargar archivo</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">80</context>
</context-group>
<note priority="1" from="description">Download archive</note>
</trans-unit>
<trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
<source>None</source>
<target state="translated">Ninguno</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">84</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">126</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">36</context>
</context-group>
<note priority="1" from="description">None</note>
</trans-unit>
<trans-unit id="4b3972c3e9485218508a95f7a4ce7758e3f09ced" datatype="html">
<source>Upload</source>
<target state="translated">Cargado</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">137</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">30</context>
</context-group>
<note priority="1" from="description">Upload</note>
</trans-unit>
<trans-unit id="6549265851868599441" datatype="html">
<source>Video</source>
<target state="translated">Video</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="347407180135731058" datatype="html">
<source>Audio</source>
<target state="translated">Audio</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">44</context>
</context-group>
</trans-unit>
<trans-unit id="8953483585652369683" datatype="html">
<source>Archive successfully imported!</source>
<target state="translated">¡Archivo importado con éxito!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit id="3159807825117518005" datatype="html">
<source>Delete archives</source>
<target state="translated">Borrar los archivos</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">152</context>
</context-group>
</trans-unit>
<trans-unit id="7022070615528435141" datatype="html">
<source>Delete</source>
<target state="translated">Borrar</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">154</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">175</context>
</context-group>
</trans-unit>
<trans-unit id="2525880134753073592" datatype="html">
<source>Successfully deleted archive items!</source>
<target state="translated">¡Elementos del archivo eliminados correctamente!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">172</context>
</context-group>
</trans-unit>
<trans-unit id="8224301330941792118" datatype="html">
<source>Failed to delete archive items!</source>
<target state="translated">¡No se pudieron eliminar los elementos del archivo!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="019d4bd6a5690f0cfa0ecf346a4e6bf7f0d8debb" datatype="html">
<source>Remove</source>
<target state="translated">Quitar</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<note priority="1" from="description">Remove</note>
</trans-unit>
<trans-unit id="6219551536751479443" datatype="html">
<source>Finished downloading</source>
<target state="translated">Descarga finalizada</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="5947241266456580665" datatype="html">
<source>Download failed</source>
<target state="translated">La descarga fracasó</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="8443034725057696949" datatype="html">
<source>Task finished</source>
<target state="translated">Tarea terminada</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="5709555629190115111" datatype="html">
<source>View task</source>
<target state="translated">Ver la tarea</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="5a105e7bd7e7db6ea211fe950fc9f317379acceb" datatype="html">
<source>No notifications available</source>
<target state="translated">No hay notificaciones disponibles</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<note priority="1" from="description">No notifications available</note>
</trans-unit>
<trans-unit id="6876310993601590130" datatype="html">
<source>Download completed</source>
<target state="translated">Descarga completa</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="6437411876967154040" datatype="html">
<source>Audio only</source>
<target state="translated">Solo el audio</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">60</context>
</context-group>
</trans-unit>
<trans-unit id="4665451070906079743" datatype="html">
<source>Favorited</source>
<target state="translated">Favorito</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">65</context>
</context-group>
</trans-unit>
<trans-unit id="6268070779441507380" datatype="html">
<source>Download Date</source>
<target state="translated">Fecha de la descarga</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="3533826530554274875" datatype="html">
<source>Upload Date</source>
<target state="translated">Fecha en la que se subió</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html">
<source>Name</source>
<target state="translated">Nombre</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="2492098975665776610" datatype="html">
<source>File Size</source>
<target state="translated">Tamaño del archivo</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="7410432243549869948" datatype="html">
<source>Duration</source>
<target state="translated">Duración</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">29</context>
</context-group>
</trans-unit>
<trans-unit id="ea2b65121b93921fe54692025da9b9e3ce779ad5" datatype="html">
<source>Task settings - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></source>
<target state="translated">Configuración de las tareas - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<note priority="1" from="description">Task settings</note>
</trans-unit>
<trans-unit id="c65dd978b3c7566551c0ebefb234c2d41942b847" datatype="html">
<source>Delete files older than</source>
<target state="translated">Eliminar los archivos anteriores a</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">6</context>
</context-group>
<note priority="1" from="description">Delete files older than</note>
</trans-unit>
<trans-unit id="56b1a3c93fb95fed1805005c561a5e431d57ffae" datatype="html">
<source>Blacklist all files</source>
<target state="translated">Lista negra de todos los archivos</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<note priority="1" from="description">Blacklist deleted files</note>
</trans-unit>
<trans-unit id="9aa1b4779a515170b297d2c0507e6ff9d2e3e0e0" datatype="html">
<source>Blacklist deleted subscription files</source>
<target state="translated">Lista negra de los archivos de la suscripción eliminados</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
<note priority="1" from="description">Blacklist deleted subscription files</note>
</trans-unit>
<trans-unit id="cdf5297d8d080a78e8b10debc5c38b7845a3cbe7" datatype="html">
<source>Do not ask for confirmation</source>
<target state="translated">No pedir confirmación</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
<note priority="1" from="description">Do not ask for confirmation</note>
</trans-unit>
<trans-unit id="9176960997786930103" datatype="html">
<source>Error for: <x id="PH" equiv-text="task['title']"/></source>
<target state="translated">Error para: <x id="PH" equiv-text="task['title']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="11a0771f88158a540a54e0e4ec5d25733d65fc0e" datatype="html">
<source>Favorite</source>
<target state="translated">Favorito</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Favorite button</note>
</trans-unit>
<trans-unit id="1d4fa01d25990f60abf21c3a451fa8ba262b7912" datatype="html">
<source>Unfavorite</source>
<target state="translated">No es favorito</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
<note priority="1" from="description">Unfavorite button</note>
</trans-unit>
<trans-unit id="5ac5a0e5ffe8e5623b40696f4c2403c17349271f" datatype="html">
<source>Sidepanel mode</source>
<target state="translated">Modo del panel lateral</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
<note priority="1" from="description">Sidepanel mode</note>
</trans-unit>
<trans-unit id="1f2809e6a99d511fdb6eaf041d785fe54d0680cc" datatype="html">
<source>File card size</source>
<target state="translated">Tamaño de la tarjeta del archivo</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">54</context>
</context-group>
<note priority="1" from="description">File card size</note>
</trans-unit>
<trans-unit id="d618f383a0ea2458eeb945a85190d4a002ea394b" datatype="html">
<source>Arg</source>
<target state="translated">Realidad virtual</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html</context>
<context context-type="linenumber">41</context>
</context-group>
<note priority="1" from="description">Arg</note>
</trans-unit>
<trans-unit id="c35ef0f03a863d33b04aae6807f140397a50f491" datatype="html">
<source>Generate RSS URL</source>
<target state="translated">Generar la url para RSS</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">306</context>
</context-group>
<note priority="1" from="description">Generate RSS URL</note>
</trans-unit>
<trans-unit id="1a9b415816364f554ee411020e65219092655271" datatype="html">
<source>Title filter</source>
<target state="translated">Filtrar por título</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
<note priority="1" from="description">Title filter</note>
</trans-unit>
<trans-unit id="9aa62bf1a535a97a4d752bbc5cf1c31af0f0c1f7" datatype="html">
<source>Supports regex</source>
<target state="translated">Admitir expresiones regulares</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">10</context>
</context-group>
<note priority="1" from="description">Supports regex</note>
</trans-unit>
<trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
<source>User</source>
<target state="translated">Usuario</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">25</context>
</context-group>
<note priority="1" from="description">User</note>
</trans-unit>
<trans-unit id="c2faa86201eab08b5b39b5437f96ab9432e125e7" datatype="html">
<source>Item limit</source>
<target state="translated">Límite del elemento</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">46</context>
</context-group>
<note priority="1" from="description">Item limit</note>
</trans-unit>
<trans-unit id="b78a98bc54259a29cf6250dbaeab5fe11fae91cf" datatype="html">
<source>Favorited</source>
<target state="translated">Favoritos</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">51</context>
</context-group>
<note priority="1" from="description">Favorited</note>
</trans-unit>
<trans-unit id="8336047719608684263" datatype="html">
<source>Unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/></source>
<target state="translated">Cancelar la suscripción a <x id="subscription name" equiv-text="this.sub['name']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="1698114086921246480" datatype="html">
<source>Unsubscribe</source>
<target state="translated">Darse de baja</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="1091872159779006651" datatype="html">
<source>You must input a time!</source>
<target state="translated">¡Debes ingresar una hora!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.ts</context>
<context context-type="linenumber">70</context>
</context-group>
</trans-unit>
<trans-unit id="d57c023a4cf63b2f12c10328c15b636ff18929aa" datatype="html">
<source>Best</source>
<target state="translated">El mejor</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.html</context>
<context context-type="linenumber">24,25</context>
</context-group>
<note priority="1" from="description">Best</note>
</trans-unit>
<trans-unit id="35cf4cdcedc8ef3f94b6100e0d86836e31dbb908" datatype="html">
<source>Force autoplay</source>
<target state="translated">Forzar la reproducción automática</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">235</context>
</context-group>
<note priority="1" from="description">Force autoplay setting</note>
</trans-unit>
<trans-unit id="37469c9f3e31d95087fa22b6c9c3bc64adf1692d" datatype="html">
<source>Enable RSS Feed</source>
<target state="translated">Activar la fuente RSS</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">304</context>
</context-group>
<note priority="1" from="description">Enable RSS Feed setting</note>
</trans-unit>
<trans-unit id="61d6b5fa4311b1c617b66dad72496f9dd43b07b4" datatype="html">
<source>Be careful enabling this with multi-user mode! User data may be exposed.</source>
<target state="translated">¡Ten cuidado al habilitar esto con el modo multiusuario! Los datos del usuario pueden estar expuestos.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">305</context>
</context-group>
<note priority="1" from="description">RSS Feed prefix</note>
</trans-unit>
<trans-unit id="fd467148a18f0921c10d116d4e0174fe29452be4" datatype="html">
<source>See documentation here.</source>
<target state="translated">Consulte la documentación aquí.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">307</context>
</context-group>
<note priority="1" from="description">RSS feed documentation</note>
</trans-unit>
<trans-unit id="8bcabdf6b16cad0313a86c7e940c5e3ad7f9f8ab" datatype="html">
<source>Notifications</source>
<target state="translated">Notificaciones</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">376</context>
</context-group>
<note priority="1" from="description">Notifications settings label</note>
</trans-unit>
<trans-unit id="c40370dc182b5e4333828b70f7478bde58bb5cfe" datatype="html">
<source>Enable notifications</source>
<target state="translated">Activar las notificaciones</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">382</context>
</context-group>
<note priority="1" from="description">Enable notifications setting</note>
</trans-unit>
<trans-unit id="33a7c6d5ff3515fa237f1fd4e43df8b65373954d" datatype="html">
<source>Enable all notifications</source>
<target state="translated">Activar toda las notificaciones</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">385</context>
</context-group>
<note priority="1" from="description">Enable all notifications setting</note>
</trans-unit>
<trans-unit id="9c562d26e041390ecc3f49dabc51cc50ebba7469" datatype="html">
<source>Allowed notification types</source>
<target state="translated">Tipos de notificaciones permitidos</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">389</context>
</context-group>
<note priority="1" from="description">Allowed notification types</note>
</trans-unit>
<trans-unit id="c5dc5fbcce45e9b1530e2a5c2baa8ebe722aef4c" datatype="html">
<source>Download complete</source>
<target state="translated">Descarga completa</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">391</context>
</context-group>
<note priority="1" from="description">Download complete</note>
</trans-unit>
<trans-unit id="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html">
<source>Download error</source>
<target state="translated">Error en la descarga</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">392</context>
</context-group>
<note priority="1" from="description">Download error</note>
</trans-unit>
<trans-unit id="2361a4f76caaa4574803fbcdca8b0a47c91cc7ed" datatype="html">
<source>Task finished</source>
<target state="translated">Tarea finalizada</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">393</context>
</context-group>
<note priority="1" from="description">Task finished</note>
</trans-unit>
<trans-unit id="9e766e11a9de375907aaf566897ecc6dac393ebc" datatype="html">
<source>Webhook URL</source>
<target state="translated">URL del webhook</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">399</context>
</context-group>
<note priority="1" from="description">webhook URL</note>
</trans-unit>
<trans-unit id="8c1bf02206fbc371ff69ab1b7e35a17ba29d169d" datatype="html">
<source>Use ntfy API</source>
<target state="translated">Utilizar la API de ntfy</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">405</context>
</context-group>
<note priority="1" from="description">Use ntfy API setting</note>
</trans-unit>
<trans-unit id="06f503e492d6dbcf59e7b9c412ca86913d718689" datatype="html">
<source>ntfy topic URL</source>
<target state="translated">URL del tema ntfy</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">409</context>
</context-group>
<note priority="1" from="description">ntfy topic URL</note>
</trans-unit>
<trans-unit id="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html">
<source>See docs here.</source>
<target state="translated">Consulta la documentación aquí.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">411</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">421</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">428</context>
</context-group>
<note priority="1" from="description">ntfy API setting hint</note>
</trans-unit>
<trans-unit id="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html">
<source>Use gotify API</source>
<target state="translated">Utilizar la Api de gotify</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">415</context>
</context-group>
<note priority="1" from="description">Use gotify API setting</note>
</trans-unit>
<trans-unit id="55f559d6f666b945479f534b0c182f70cd0a8a69" datatype="html">
<source>Gotify server URL</source>
<target state="translated">URL del servidor Gotify</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">419</context>
</context-group>
<note priority="1" from="description">Gotify server URL</note>
</trans-unit>
<trans-unit id="b770c48628d98cb4633d6a17e3f0ba0265376af5" datatype="html">
<source>Gotify app token</source>
<target state="translated">Token de la aplicación Gotify</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">426</context>
</context-group>
<note priority="1" from="description">Gotify app token</note>
</trans-unit>
<trans-unit id="38992954440d6afb54aeb58af12ca0123ee5e26e" datatype="html">
<source>Use Telegram API</source>
<target state="translated">Utilizar la API de Telegram</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">432</context>
</context-group>
<note priority="1" from="description">Use Telegram API setting</note>
</trans-unit>
<trans-unit id="2e076ff9866213d0815961c494aa48b177046b9d" datatype="html">
<source>Telegram bot token</source>
<target state="translated">Tomen del bot de Telegram</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">436</context>
</context-group>
<note priority="1" from="description">Telegram bot token</note>
</trans-unit>
<trans-unit id="eeb0ba2e4743901d8f5eebd9a3529aa1f236c608" datatype="html">
<source>Create bot here.</source>
<target state="translated">Crear un bot aquí.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">438</context>
</context-group>
<note priority="1" from="description">Telegram bot create link</note>
</trans-unit>
<trans-unit id="144e1a21ebe8fa238f88d2ac27515ed711cfc9a0" datatype="html">
<source>Telegram chat ID</source>
<target state="translated">ID del chat de Telegram</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">443</context>
</context-group>
<note priority="1" from="description">Telegram chat ID</note>
</trans-unit>
<trans-unit id="3e420c675b8f3f3702576d52e8bb6e8e1d3feda0" datatype="html">
<source>How do I get the chat ID?</source>
<target state="translated">¿Cómo obtengo la identificación del chat?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">445</context>
</context-group>
<note priority="1" from="description">Telegram chat ID help</note>
</trans-unit>
<trans-unit id="a4ed8eba1e057e67d5c2d87b52230f182b3dae4e" datatype="html">
<source>Restart required.</source>
<target state="translated">Reinicio requerido.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">465</context>
</context-group>
<note priority="1" from="description">Restart required hint</note>
</trans-unit>
<trans-unit id="6785427850041119037" datatype="html">
<source>Delete category</source>
<target state="translated">Borrar la categoría</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">173</context>
</context-group>
</trans-unit>
<trans-unit id="2481374649045841364" datatype="html">
<source>Would you like to delete <x id="category name" equiv-text="category['name']"/>?</source>
<target state="translated">¿Deseas eliminar <x id="category name" equiv-text="category['name']"/>?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="3371159074051387771" datatype="html">
<source>Failed to delete <x id="category name" equiv-text="category['name']"/>!</source>
<target state="translated">¡No se ha podido eliminar <x id="category name" equiv-text="category['name']"/>!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">187</context>
</context-group>
</trans-unit>
<trans-unit id="8c6e24eab969d9f63a8a0e9d617aee3b99e28ae6" datatype="html">
<source>Play all</source>
<target state="translated">Reproducir todo</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
<context context-type="linenumber">17</context>
</context-group>
<note priority="1" from="description">Play all</note>
</trans-unit>
<trans-unit id="674a999dd48d7da565ffdd105602261b8a4761ea" datatype="html">
<source>Download zip</source>
<target state="translated">Descargar en un archivo zip</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
<note priority="1" from="description">Download zip</note>
</trans-unit>
<trans-unit id="0ed98b4c6ec1db6708a963e8a2699478ac97f55c" datatype="html">
<source>Add subscription</source>
<target state="translated">Añadir suscripción</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscriptions/subscriptions.component.html</context>
<context context-type="linenumber">60</context>
</context-group>
<note priority="1" from="description">Add subscription</note>
</trans-unit>
<trans-unit id="28da11220a3377ddce3c7948825d33101f142782" datatype="html">
<source>Extractor</source>
<target state="translated">Extractor</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">57</context>
</context-group>
<note priority="1" from="description">Extractor</note>
</trans-unit>
<trans-unit id="8425787787095143143" datatype="html">
<source>Would you like to delete <x id="selected archives amount" equiv-text="this.selection.selected.length"/> archive(s)?</source>
<target state="translated">¿ Quieres borrar el(los) archivo(s) de <x id="selected archives amount" equiv-text="this.selection.selected.length"/> ?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">153</context>
</context-group>
</trans-unit>
<trans-unit id="8564202903947049539" datatype="html">
<source>Play</source>
<target state="translated">Reproducir</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="8643601595923420698" datatype="html">
<source>Retry download</source>
<target state="translated">Reintertar la descarga</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="8571838164752006148" datatype="html">
<source>View error</source>
<target state="translated">Ver el error</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="1879058637439215882" datatype="html">
<source>Download error</source>
<target state="translated">Error al descargar</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="7911845622864460134" datatype="html">
<source>Video only</source>
<target state="translated">Solo el video</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">55</context>
</context-group>
</trans-unit>
<trans-unit id="4578192247039196794" datatype="html">
<source>Task</source>
<target state="translated">Tarea</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="5000203534763292992" datatype="html">
<source>Download restarted!</source>
<target state="translated">¡Descarga reiniciada!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="7b4585a9072f3c1292972c14a3d0e14978fbfc9c" datatype="html">
<source>Delete old files:</source>
<target state="translated">Eliminar los archivos antiguos:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.html</context>
<context context-type="linenumber">66</context>
</context-group>
<note priority="1" from="description">Delete old files</note>
</trans-unit>
<trans-unit id="784837056777689544" datatype="html">
<source>Would you like to unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/>?</source>
<target state="translated">¿Deseas anular tu suscripción a <x id="subscription name" equiv-text="this.sub['name']"/>?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="7332320960988475089" datatype="html">
<source>Successfully deleted <x id="category name" equiv-text="category['name']"/>!</source>
<target state="translated">¡Se ha eliminado correctamente <x id="category name" equiv-text="category['name']"/>!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">183</context>
</context-group>
</trans-unit>
<trans-unit id="7cedb649779673568447b994463b2882c4e0436a" datatype="html">
<source>Slack Webhook URL</source>
<target state="translated">URL del webhook de Slack</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">397</context>
</context-group>
<note priority="1" from="description">Slack Webhook URL</note>
</trans-unit>
<trans-unit id="3264d82792954815be755b3da01e2625458711dc" datatype="html">
<source>Discord Webhook URL</source>
<target state="translated">URL del webhook de Discord</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">390</context>
</context-group>
<note priority="1" from="description">Discord Webhook URL</note>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@@ -3757,6 +3757,33 @@
</context-group> </context-group>
<note priority="1" from="description">Select a version</note> <note priority="1" from="description">Select a version</note>
</trans-unit> </trans-unit>
<trans-unit id="2361a4f76caaa4574803fbcdca8b0a47c91cc7ed" datatype="html">
<source>Task finished</source>
<target state="translated">Zadanie zakończone</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">377</context>
</context-group>
<note priority="1" from="description">Task finished</note>
</trans-unit>
<trans-unit id="c5dc5fbcce45e9b1530e2a5c2baa8ebe722aef4c" datatype="html">
<source>Download complete</source>
<target state="translated">Pobieranie zakończone</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">375</context>
</context-group>
<note priority="1" from="description">Download complete</note>
</trans-unit>
<trans-unit id="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html">
<source>Download error</source>
<target state="translated">Błąd pobierania</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">376</context>
</context-group>
<note priority="1" from="description">Download error</note>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>