mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-10 06:41:00 +03:00
Compare commits
123 Commits
docker-fix
...
rebuild-da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9c0247480 | ||
|
|
2fcf5364d8 | ||
|
|
a38fb0e2e0 | ||
|
|
e6050969ec | ||
|
|
958300c281 | ||
|
|
84c2b2769b | ||
|
|
e145c9c992 | ||
|
|
078408236c | ||
|
|
2adbc0a02c | ||
|
|
fe95f04c18 | ||
|
|
9b3816afce | ||
|
|
07874d9241 | ||
|
|
03122b4c81 | ||
|
|
3deb1e8459 | ||
|
|
9fa1aab1e5 | ||
|
|
80b41af620 | ||
|
|
ab5d8dc5ca | ||
|
|
4b55c39f39 | ||
|
|
3ca296f195 | ||
|
|
d4fa640f0f | ||
|
|
427eecf214 | ||
|
|
4f54e408a5 | ||
|
|
9e481bbd5f | ||
|
|
78b29a76b8 | ||
|
|
0342d18f76 | ||
|
|
70754c580c | ||
|
|
e58b0b8638 | ||
|
|
df8f8070ca | ||
|
|
0b8ca31594 | ||
|
|
658a76dc1c | ||
|
|
f363ec5db6 | ||
|
|
f36d675abf | ||
|
|
35fcf44e1a | ||
|
|
bad6080730 | ||
|
|
2a7b62272e | ||
|
|
be74377a08 | ||
|
|
808c7e2112 | ||
|
|
d6f39d37b5 | ||
|
|
0c46b044da | ||
|
|
e573f34cea | ||
|
|
52e32d4f0f | ||
|
|
adb5f2256e | ||
|
|
59bf6ff86d | ||
|
|
5ce2e2a35d | ||
|
|
68fbde8907 | ||
|
|
62bccb3349 | ||
|
|
90d9ac025a | ||
|
|
07903131f9 | ||
|
|
ec3bb3e738 | ||
|
|
18fcf4eb61 | ||
|
|
19f35d6af4 | ||
|
|
3a918b7059 | ||
|
|
7e7da6c0bc | ||
|
|
f9f7204deb | ||
|
|
8827d9f3de | ||
|
|
42bc255d6c | ||
|
|
2df3b9cbfd | ||
|
|
b859d08d86 | ||
|
|
e7325b2dc2 | ||
|
|
21463762ce | ||
|
|
b06f6a81bb | ||
|
|
82c8146032 | ||
|
|
6f13eab550 | ||
|
|
9d2d70b194 | ||
|
|
4e04ceae16 | ||
|
|
5eec5ac082 | ||
|
|
5253ce8793 | ||
|
|
33a99d9c8d | ||
|
|
0e5c78db0d | ||
|
|
9a08fc6140 | ||
|
|
e7b9dfd312 | ||
|
|
1e2922559c | ||
|
|
cfbee6d6f1 | ||
|
|
c75d58efd5 | ||
|
|
efbf395368 | ||
|
|
dab9fc83ba | ||
|
|
e086bbc301 | ||
|
|
0b3a21b383 | ||
|
|
f973426bd2 | ||
|
|
a4c78e3a3d | ||
|
|
50d3bc183b | ||
|
|
5a379a6a2b | ||
|
|
71692f6b13 | ||
|
|
1746b08d4c | ||
|
|
3bc0ec8bb5 | ||
|
|
2df4dc1bfc | ||
|
|
0e190fca2a | ||
|
|
5aea0b7a3d | ||
|
|
d76aaf83f6 | ||
|
|
a996b9f0d2 | ||
|
|
d3b88412c6 | ||
|
|
6cee892e18 | ||
|
|
e2438a236b | ||
|
|
7a4ae052ed | ||
|
|
b65a7b3dd4 | ||
|
|
955c401f0b | ||
|
|
f0a34df7c6 | ||
|
|
e2c68713ba | ||
|
|
24cabc1f02 | ||
|
|
1edcfca6c3 | ||
|
|
e7fa25cf38 | ||
|
|
527b1f1cb9 | ||
|
|
24d8072eb5 | ||
|
|
c81bf980ca | ||
|
|
a91381720f | ||
|
|
edd4a0928c | ||
|
|
770916492e | ||
|
|
6400b807c2 | ||
|
|
3a7e2d9d0f | ||
|
|
ca5381fe0f | ||
|
|
26988bd607 | ||
|
|
bd8d91ebe5 | ||
|
|
27f05dbae3 | ||
|
|
c7bf1d0e27 | ||
|
|
57be0a032e | ||
|
|
6fe4b22efc | ||
|
|
af2d583924 | ||
|
|
c61d51be76 | ||
|
|
575f7eed4e | ||
|
|
142d708ee3 | ||
|
|
2e52ec22e0 | ||
|
|
efdd0dd228 | ||
|
|
415c97cb09 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -15,9 +15,9 @@ jobs:
|
|||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: setup node
|
- name: setup node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||||
- name: create-json
|
- name: create-json
|
||||||
id: create-json
|
id: create-json
|
||||||
uses: jsdaniell/create-json@1.1.2
|
uses: jsdaniell/create-json@v1.2.2
|
||||||
with:
|
with:
|
||||||
name: "version.json"
|
name: "version.json"
|
||||||
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
|
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
|
||||||
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
|
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
|
||||||
- name: upload build artifact
|
- name: upload build artifact
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: youtubedl-material
|
name: youtubedl-material
|
||||||
path: build
|
path: build
|
||||||
|
|||||||
17
.github/workflows/docker-pr.yml
vendored
17
.github/workflows/docker-pr.yml
vendored
@@ -18,10 +18,21 @@ jobs:
|
|||||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||||
- name: create-json
|
- name: create-json
|
||||||
id: create-json
|
id: create-json
|
||||||
uses: jsdaniell/create-json@1.1.2
|
uses: jsdaniell/create-json@v1.2.2
|
||||||
with:
|
with:
|
||||||
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
|
||||||
|
|||||||
8
.github/workflows/docker-release.yml
vendored
8
.github/workflows/docker-release.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
- name: create-json
|
- name: create-json
|
||||||
id: create-json
|
id: create-json
|
||||||
uses: jsdaniell/create-json@1.1.2
|
uses: jsdaniell/create-json@v1.2.2
|
||||||
with:
|
with:
|
||||||
name: "version.json"
|
name: "version.json"
|
||||||
json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||||
@@ -60,10 +60,10 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
- name: setup multi-arch docker build
|
- name: setup multi-arch docker build
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@@ -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 }}
|
||||||
|
|||||||
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
- name: create-json
|
- name: create-json
|
||||||
id: create-json
|
id: create-json
|
||||||
uses: jsdaniell/create-json@1.1.2
|
uses: jsdaniell/create-json@v1.2.2
|
||||||
with:
|
with:
|
||||||
name: "version.json"
|
name: "version.json"
|
||||||
json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
- name: setup multi-arch docker build
|
- name: setup multi-arch docker build
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Generate Docker image metadata
|
- name: Generate Docker image metadata
|
||||||
id: docker-meta
|
id: docker-meta
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
type=sha,prefix=sha-,format=short
|
type=sha,prefix=sha-,format=short
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@@ -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
14
.vscode/launch.json
vendored
@@ -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
38
DEVELOPMENT.md
Normal 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.
|
||||||
57
Dockerfile
57
Dockerfile
@@ -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 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/" ]
|
||||||
@@ -37,6 +50,8 @@ COPY [ "src/", "/build/src/" ]
|
|||||||
RUN npm install && \
|
RUN npm install && \
|
||||||
npm run build && \
|
npm run build && \
|
||||||
ls -al /build/backend/public
|
ls -al /build/backend/public
|
||||||
|
RUN npm uninstall -g @angular/cli
|
||||||
|
RUN rm -rf node_modules
|
||||||
|
|
||||||
|
|
||||||
# Install backend deps
|
# Install backend deps
|
||||||
@@ -47,21 +62,35 @@ RUN npm config set strict-ssl false && \
|
|||||||
npm install --prod && \
|
npm install --prod && \
|
||||||
ls -al
|
ls -al
|
||||||
|
|
||||||
|
#FROM base as python
|
||||||
|
# armv7 need build from source
|
||||||
|
#WORKDIR /app
|
||||||
|
#COPY docker-utils/GetTwitchDownloader.py .
|
||||||
|
#RUN apt update && \
|
||||||
|
# apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip python3-dev build-essential libffi-dev && \
|
||||||
|
# apt clean && \
|
||||||
|
# rm -rf /var/lib/apt/lists/*
|
||||||
|
#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 tdh-tcd 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"]
|
||||||
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"]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -28,13 +28,28 @@ Dark mode:
|
|||||||
|
|
||||||
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
||||||
|
|
||||||
Debian/Ubuntu:
|
Required dependencies:
|
||||||
|
|
||||||
|
* Node.js 16
|
||||||
|
* Python
|
||||||
|
|
||||||
|
Optional dependencies:
|
||||||
|
|
||||||
|
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
||||||
|
* [Twitch Downloader CLI](https://github.com/lay295/TwitchDownloader) (for downloading Twitch VOD chats)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Debian/Ubuntu</summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||||
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
|
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
|
||||||
```
|
```
|
||||||
|
|
||||||
CentOS 7:
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>CentOS 7</summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo yum install epel-release
|
sudo yum install epel-release
|
||||||
@@ -42,13 +57,11 @@ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfu
|
|||||||
sudo yum install centos-release-scl-rh
|
sudo yum install centos-release-scl-rh
|
||||||
sudo yum install rh-nodejs12
|
sudo yum install rh-nodejs12
|
||||||
scl enable rh-nodejs12 bash
|
scl enable rh-nodejs12 bash
|
||||||
|
curl -fsSL https://rpm.nodesource.com/setup_16.x | sudo bash -
|
||||||
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional dependencies:
|
</details>
|
||||||
|
|
||||||
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
|
||||||
* [tcd](https://github.com/PetterKraabol/Twitch-Chat-Downloader) (for downloading Twitch VOD chats)
|
|
||||||
|
|
||||||
### Installing
|
### Installing
|
||||||
|
|
||||||
@@ -72,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`.
|
||||||
|
|
||||||
|
|||||||
100
backend/app.js
100
backend/app.js
@@ -2,7 +2,6 @@ const { uuid } = require('uuidv4');
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const auth_api = require('./authentication/auth');
|
const auth_api = require('./authentication/auth');
|
||||||
const winston = require('winston');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
@@ -19,6 +18,7 @@ const CONSTS = require('./consts')
|
|||||||
const read_last_lines = require('read-last-lines');
|
const read_last_lines = require('read-last-lines');
|
||||||
const ps = require('ps-node');
|
const ps = require('ps-node');
|
||||||
const Feed = require('feed').Feed;
|
const Feed = require('feed').Feed;
|
||||||
|
const session = require('express-session');
|
||||||
|
|
||||||
// needed if bin/details somehow gets deleted
|
// needed if bin/details somehow gets deleted
|
||||||
if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2000.06.06","path":"node_modules\\youtube-dl\\bin\\youtube-dl.exe","exec":"youtube-dl.exe","downloader":"youtube-dl"})
|
if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2000.06.06","path":"node_modules\\youtube-dl\\bin\\youtube-dl.exe","exec":"youtube-dl.exe","downloader":"youtube-dl"})
|
||||||
@@ -34,6 +34,7 @@ const categories_api = require('./categories');
|
|||||||
const twitch_api = require('./twitch');
|
const twitch_api = require('./twitch');
|
||||||
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');
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
@@ -162,6 +163,8 @@ app.use(bodyParser.json());
|
|||||||
|
|
||||||
// use passport
|
// use passport
|
||||||
app.use(auth_api.passport.initialize());
|
app.use(auth_api.passport.initialize());
|
||||||
|
app.use(session({ secret: uuid(), resave: true, saveUninitialized: true }))
|
||||||
|
app.use(auth_api.passport.session());
|
||||||
|
|
||||||
// actual functions
|
// actual functions
|
||||||
|
|
||||||
@@ -172,10 +175,10 @@ async function checkMigrations() {
|
|||||||
if (!simplified_db_migration_complete) {
|
if (!simplified_db_migration_complete) {
|
||||||
logger.info('Beginning migration: 4.1->4.2+')
|
logger.info('Beginning migration: 4.1->4.2+')
|
||||||
let success = await simplifyDBFileStructure();
|
let success = await simplifyDBFileStructure();
|
||||||
success = success && await db_api.addMetadataPropertyToDB('view_count');
|
success = success && await files_api.addMetadataPropertyToDB('view_count');
|
||||||
success = success && await db_api.addMetadataPropertyToDB('description');
|
success = success && await files_api.addMetadataPropertyToDB('description');
|
||||||
success = success && await db_api.addMetadataPropertyToDB('height');
|
success = success && await files_api.addMetadataPropertyToDB('height');
|
||||||
success = success && await db_api.addMetadataPropertyToDB('abr');
|
success = success && await files_api.addMetadataPropertyToDB('abr');
|
||||||
// sets migration to complete
|
// sets migration to complete
|
||||||
db.set('simplified_db_migration_complete', true).write();
|
db.set('simplified_db_migration_complete', true).write();
|
||||||
if (success) { logger.info('4.1->4.2+ migration complete!'); }
|
if (success) { logger.info('4.1->4.2+ migration complete!'); }
|
||||||
@@ -532,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() {
|
||||||
@@ -723,7 +727,7 @@ const optionalJwt = async function (req, res, next) {
|
|||||||
const uuid = using_body ? req.body.uuid : req.query.uuid;
|
const uuid = using_body ? req.body.uuid : req.query.uuid;
|
||||||
const uid = using_body ? req.body.uid : req.query.uid;
|
const uid = using_body ? req.body.uid : req.query.uid;
|
||||||
const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id;
|
const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id;
|
||||||
const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : await db_api.getPlaylist(playlist_id, uuid, true);
|
const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : await files_api.getPlaylist(playlist_id, uuid, true);
|
||||||
if (file) {
|
if (file) {
|
||||||
req.can_watch = true;
|
req.can_watch = true;
|
||||||
return next();
|
return next();
|
||||||
@@ -934,7 +938,7 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
|||||||
const sub_id = req.body.sub_id;
|
const sub_id = req.body.sub_id;
|
||||||
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
const {files, file_count} = await db_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
|
const {files, file_count} = await files_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
files: files,
|
files: files,
|
||||||
@@ -1100,7 +1104,7 @@ app.post('/api/incrementViewCount', async (req, res) => {
|
|||||||
uuid = req.user.uid;
|
uuid = req.user.uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file_obj = await db_api.getVideo(file_uid, uuid, sub_id);
|
const file_obj = await files_api.getVideo(file_uid, uuid, sub_id);
|
||||||
|
|
||||||
const current_view_count = file_obj && file_obj['local_view_count'] ? file_obj['local_view_count'] : 0;
|
const current_view_count = file_obj && file_obj['local_view_count'] ? file_obj['local_view_count'] : 0;
|
||||||
const new_view_count = current_view_count + 1;
|
const new_view_count = current_view_count + 1;
|
||||||
@@ -1228,7 +1232,7 @@ app.post('/api/deleteSubscriptionFile', optionalJwt, async (req, res) => {
|
|||||||
let deleteForever = req.body.deleteForever;
|
let deleteForever = req.body.deleteForever;
|
||||||
let file_uid = req.body.file_uid;
|
let file_uid = req.body.file_uid;
|
||||||
|
|
||||||
let success = await db_api.deleteFile(file_uid, deleteForever);
|
let success = await files_api.deleteFile(file_uid, deleteForever);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
res.send({
|
res.send({
|
||||||
@@ -1316,7 +1320,7 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
|
|||||||
let playlistName = req.body.playlistName;
|
let playlistName = req.body.playlistName;
|
||||||
let uids = req.body.uids;
|
let uids = req.body.uids;
|
||||||
|
|
||||||
const new_playlist = await db_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null);
|
const new_playlist = await files_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null);
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
new_playlist: new_playlist,
|
new_playlist: new_playlist,
|
||||||
@@ -1329,13 +1333,13 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
|
|||||||
let uuid = req.body.uuid ? req.body.uuid : (req.user && req.user.uid ? req.user.uid : null);
|
let uuid = req.body.uuid ? req.body.uuid : (req.user && req.user.uid ? req.user.uid : null);
|
||||||
let include_file_metadata = req.body.include_file_metadata;
|
let include_file_metadata = req.body.include_file_metadata;
|
||||||
|
|
||||||
const playlist = await db_api.getPlaylist(playlist_id, uuid);
|
const playlist = await files_api.getPlaylist(playlist_id, uuid);
|
||||||
const file_objs = [];
|
const file_objs = [];
|
||||||
|
|
||||||
if (playlist && include_file_metadata) {
|
if (playlist && include_file_metadata) {
|
||||||
for (let i = 0; i < playlist['uids'].length; i++) {
|
for (let i = 0; i < playlist['uids'].length; i++) {
|
||||||
const uid = playlist['uids'][i];
|
const uid = playlist['uids'][i];
|
||||||
const file_obj = await db_api.getVideo(uid, uuid);
|
const file_obj = await files_api.getVideo(uid, uuid);
|
||||||
if (file_obj) file_objs.push(file_obj);
|
if (file_obj) file_objs.push(file_obj);
|
||||||
// TODO: remove file from playlist if could not be found
|
// TODO: remove file from playlist if could not be found
|
||||||
}
|
}
|
||||||
@@ -1373,7 +1377,7 @@ app.post('/api/addFileToPlaylist', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
playlist.uids.push(file_uid);
|
playlist.uids.push(file_uid);
|
||||||
|
|
||||||
let success = await db_api.updatePlaylist(playlist);
|
let success = await files_api.updatePlaylist(playlist);
|
||||||
res.send({
|
res.send({
|
||||||
success: success
|
success: success
|
||||||
});
|
});
|
||||||
@@ -1381,7 +1385,7 @@ app.post('/api/addFileToPlaylist', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
|
app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
|
||||||
let playlist = req.body.playlist;
|
let playlist = req.body.playlist;
|
||||||
let success = await db_api.updatePlaylist(playlist, req.user && req.user.uid);
|
let success = await files_api.updatePlaylist(playlist, req.user && req.user.uid);
|
||||||
res.send({
|
res.send({
|
||||||
success: success
|
success: success
|
||||||
});
|
});
|
||||||
@@ -1411,7 +1415,7 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => {
|
|||||||
const blacklistMode = req.body.blacklistMode;
|
const blacklistMode = req.body.blacklistMode;
|
||||||
|
|
||||||
let wasDeleted = false;
|
let wasDeleted = false;
|
||||||
wasDeleted = await db_api.deleteFile(uid, blacklistMode);
|
wasDeleted = await files_api.deleteFile(uid, blacklistMode);
|
||||||
res.send(wasDeleted);
|
res.send(wasDeleted);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1443,7 +1447,7 @@ app.post('/api/deleteAllFiles', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
let wasDeleted = false;
|
let wasDeleted = false;
|
||||||
wasDeleted = await db_api.deleteFile(files[i].uid, blacklistMode);
|
wasDeleted = await files_api.deleteFile(files[i].uid, blacklistMode);
|
||||||
if (wasDeleted) {
|
if (wasDeleted) {
|
||||||
delete_count++;
|
delete_count++;
|
||||||
}
|
}
|
||||||
@@ -1469,10 +1473,10 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
|
|||||||
if (playlist_id) {
|
if (playlist_id) {
|
||||||
zip_file_generated = true;
|
zip_file_generated = true;
|
||||||
const playlist_files_to_download = [];
|
const playlist_files_to_download = [];
|
||||||
const playlist = await db_api.getPlaylist(playlist_id, uuid);
|
const playlist = await files_api.getPlaylist(playlist_id, uuid);
|
||||||
for (let i = 0; i < playlist['uids'].length; i++) {
|
for (let i = 0; i < playlist['uids'].length; i++) {
|
||||||
const playlist_file_uid = playlist['uids'][i];
|
const playlist_file_uid = playlist['uids'][i];
|
||||||
const file_obj = await db_api.getVideo(playlist_file_uid, uuid);
|
const file_obj = await files_api.getVideo(playlist_file_uid, uuid);
|
||||||
playlist_files_to_download.push(file_obj);
|
playlist_files_to_download.push(file_obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1486,7 +1490,7 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
|
|||||||
// generate zip
|
// generate zip
|
||||||
file_path_to_download = await utils.createContainerZipFile(sub['name'], sub_files_to_download);
|
file_path_to_download = await utils.createContainerZipFile(sub['name'], sub_files_to_download);
|
||||||
} else {
|
} else {
|
||||||
const file_obj = await db_api.getVideo(uid, uuid, sub_id)
|
const file_obj = await files_api.getVideo(uid, uuid, sub_id)
|
||||||
file_path_to_download = file_obj.path;
|
file_path_to_download = file_obj.path;
|
||||||
}
|
}
|
||||||
if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download);
|
if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download);
|
||||||
@@ -1633,12 +1637,12 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||||
if (!multiUserMode || req.isAuthenticated() || req.can_watch) {
|
if (!multiUserMode || req.isAuthenticated() || req.can_watch) {
|
||||||
file_obj = await db_api.getVideo(uid, uuid, sub_id);
|
file_obj = await files_api.getVideo(uid, uuid, sub_id);
|
||||||
if (file_obj) file_path = file_obj['path'];
|
if (file_obj) file_path = file_obj['path'];
|
||||||
else file_path = null;
|
else file_path = null;
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(file_path)) {
|
if (!fs.existsSync(file_path)) {
|
||||||
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj.id}`);
|
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
|
||||||
}
|
}
|
||||||
const stat = fs.statSync(file_path);
|
const stat = fs.statSync(file_path);
|
||||||
const fileSize = stat.size;
|
const fileSize = stat.size;
|
||||||
@@ -1925,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
|
||||||
@@ -1979,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);
|
||||||
@@ -2031,7 +2049,7 @@ app.post('/api/changeRolePermissions', optionalJwt, async (req, res) => {
|
|||||||
// notifications
|
// notifications
|
||||||
|
|
||||||
app.post('/api/getNotifications', optionalJwt, async (req, res) => {
|
app.post('/api/getNotifications', optionalJwt, async (req, res) => {
|
||||||
const uuid = req.user.uid;
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
const notifications = await db_api.getRecords('notifications', {user_uid: uuid});
|
const notifications = await db_api.getRecords('notifications', {user_uid: uuid});
|
||||||
|
|
||||||
@@ -2040,7 +2058,7 @@ app.post('/api/getNotifications', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
// set notifications to read
|
// set notifications to read
|
||||||
app.post('/api/setNotificationsToRead', optionalJwt, async (req, res) => {
|
app.post('/api/setNotificationsToRead', optionalJwt, async (req, res) => {
|
||||||
const uuid = req.user.uid;
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
const success = await db_api.updateRecords('notifications', {user_uid: uuid}, {read: true});
|
const success = await db_api.updateRecords('notifications', {user_uid: uuid}, {read: true});
|
||||||
|
|
||||||
@@ -2048,7 +2066,7 @@ app.post('/api/setNotificationsToRead', optionalJwt, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/deleteNotification', optionalJwt, async (req, res) => {
|
app.post('/api/deleteNotification', optionalJwt, async (req, res) => {
|
||||||
const uid = req.body.uid;
|
const uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
const success = await db_api.removeRecord('notifications', {uid: uid});
|
const success = await db_api.removeRecord('notifications', {uid: uid});
|
||||||
|
|
||||||
@@ -2056,7 +2074,7 @@ app.post('/api/deleteNotification', optionalJwt, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => {
|
app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => {
|
||||||
const uuid = req.user.uid;
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
const success = await db_api.removeAllRecords('notifications', {user_uid: uuid});
|
const success = await db_api.removeAllRecords('notifications', {user_uid: uuid});
|
||||||
|
|
||||||
@@ -2081,7 +2099,7 @@ app.get('/api/rss', async function (req, res) {
|
|||||||
const sub_id = req.query.sub_id ? decodeURIComponent(req.query.sub_id) : null;
|
const sub_id = req.query.sub_id ? decodeURIComponent(req.query.sub_id) : null;
|
||||||
const uuid = req.query.uuid ? decodeURIComponent(req.query.uuid) : null;
|
const uuid = req.query.uuid ? decodeURIComponent(req.query.uuid) : null;
|
||||||
|
|
||||||
const {files} = await db_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
|
const {files} = await files_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
|
||||||
|
|
||||||
const feed = new Feed({
|
const feed = new Feed({
|
||||||
title: 'Downloads',
|
title: 'Downloads',
|
||||||
|
|||||||
@@ -23,7 +23,12 @@
|
|||||||
"download_only_mode": false,
|
"download_only_mode": false,
|
||||||
"allow_autoplay": true,
|
"allow_autoplay": true,
|
||||||
"enable_downloads_manager": true,
|
"enable_downloads_manager": true,
|
||||||
"allow_playlist_categorization": true
|
"allow_playlist_categorization": true,
|
||||||
|
"force_autoplay": false,
|
||||||
|
"enable_notifications": true,
|
||||||
|
"enable_all_notifications": true,
|
||||||
|
"allowed_notification_types": [],
|
||||||
|
"enable_rss_feed": false
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"use_API_key": false,
|
"use_API_key": false,
|
||||||
@@ -35,7 +40,18 @@
|
|||||||
"twitch_client_secret": "",
|
"twitch_client_secret": "",
|
||||||
"twitch_auto_download_chat": false,
|
"twitch_auto_download_chat": false,
|
||||||
"use_sponsorblock_API": false,
|
"use_sponsorblock_API": false,
|
||||||
"generate_NFO_files": false
|
"generate_NFO_files": false,
|
||||||
|
"use_ntfy_API": false,
|
||||||
|
"ntfy_topic_URL": "",
|
||||||
|
"use_gotify_API": false,
|
||||||
|
"gotify_server_URL": "",
|
||||||
|
"gotify_app_token": "",
|
||||||
|
"use_telegram_API": false,
|
||||||
|
"telegram_bot_token": "",
|
||||||
|
"telegram_chat_id": "",
|
||||||
|
"webhook_URL": "",
|
||||||
|
"discord_webhook_URL": "",
|
||||||
|
"slack_webhook_URL": ""
|
||||||
},
|
},
|
||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -208,9 +208,6 @@ const DEFAULT_CONFIG = {
|
|||||||
"API_key": "",
|
"API_key": "",
|
||||||
"use_youtube_API": false,
|
"use_youtube_API": false,
|
||||||
"youtube_API_key": "",
|
"youtube_API_key": "",
|
||||||
"use_twitch_API": false,
|
|
||||||
"twitch_client_ID": "",
|
|
||||||
"twitch_client_secret": "",
|
|
||||||
"twitch_auto_download_chat": false,
|
"twitch_auto_download_chat": false,
|
||||||
"use_sponsorblock_API": false,
|
"use_sponsorblock_API": false,
|
||||||
"generate_NFO_files": false,
|
"generate_NFO_files": false,
|
||||||
@@ -222,7 +219,9 @@ const DEFAULT_CONFIG = {
|
|||||||
"use_telegram_API": false,
|
"use_telegram_API": false,
|
||||||
"telegram_bot_token": "",
|
"telegram_bot_token": "",
|
||||||
"telegram_chat_id": "",
|
"telegram_chat_id": "",
|
||||||
"webhook_URL": ""
|
"webhook_URL": "",
|
||||||
|
"discord_webhook_URL": "",
|
||||||
|
"slack_webhook_URL": "",
|
||||||
},
|
},
|
||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
|
|||||||
@@ -110,18 +110,6 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_youtube_api_key',
|
'key': 'ytdl_youtube_api_key',
|
||||||
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
||||||
},
|
},
|
||||||
'ytdl_use_twitch_api': {
|
|
||||||
'key': 'ytdl_use_twitch_api',
|
|
||||||
'path': 'YoutubeDLMaterial.API.use_twitch_API'
|
|
||||||
},
|
|
||||||
'ytdl_twitch_client_id': {
|
|
||||||
'key': 'ytdl_twitch_client_id',
|
|
||||||
'path': 'YoutubeDLMaterial.API.twitch_client_ID'
|
|
||||||
},
|
|
||||||
'ytdl_twitch_client_secret': {
|
|
||||||
'key': 'ytdl_twitch_client_secret',
|
|
||||||
'path': 'YoutubeDLMaterial.API.twitch_client_secret'
|
|
||||||
},
|
|
||||||
'ytdl_twitch_auto_download_chat': {
|
'ytdl_twitch_auto_download_chat': {
|
||||||
'key': 'ytdl_twitch_auto_download_chat',
|
'key': 'ytdl_twitch_auto_download_chat',
|
||||||
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
||||||
@@ -170,6 +158,14 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_webhook_url',
|
'key': 'ytdl_webhook_url',
|
||||||
'path': 'YoutubeDLMaterial.API.webhook_URL'
|
'path': 'YoutubeDLMaterial.API.webhook_URL'
|
||||||
},
|
},
|
||||||
|
'ytdl_discord_webhook_url': {
|
||||||
|
'key': 'ytdl_discord_webhook_url',
|
||||||
|
'path': 'YoutubeDLMaterial.API.discord_webhook_URL'
|
||||||
|
},
|
||||||
|
'ytdl_slack_webhook_url': {
|
||||||
|
'key': 'ytdl_slack_webhook_url',
|
||||||
|
'path': 'YoutubeDLMaterial.API.slack_webhook_URL'
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// Themes
|
// Themes
|
||||||
@@ -351,7 +347,11 @@ 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);
|
||||||
|
|
||||||
|
exports.ICON_URL = 'https://i.imgur.com/IKOlr0N.png';
|
||||||
|
|
||||||
exports.CURRENT_VERSION = 'v4.3.1';
|
exports.CURRENT_VERSION = 'v4.3.1';
|
||||||
|
|||||||
385
backend/db.js
385
backend/db.js
@@ -1,11 +1,11 @@
|
|||||||
var fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
const { MongoClient } = require("mongodb");
|
const { MongoClient } = require("mongodb");
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
var utils = require('./utils')
|
const utils = require('./utils')
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
|
||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
@@ -167,82 +167,9 @@ exports._connectToDB = async (custom_connection_string = null) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.registerFileDB = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
|
exports.setVideoProperty = async (file_uid, assignment_obj) => {
|
||||||
if (!file_object) file_object = generateFileObject(file_path, type);
|
// TODO: check if video exists, throw error if not
|
||||||
if (!file_object) {
|
await exports.updateRecord('files', {uid: file_uid}, assignment_obj);
|
||||||
logger.error(`Could not find associated JSON file for ${type} file ${file_path}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.fixVideoMetadataPerms(file_path, type);
|
|
||||||
|
|
||||||
// add thumbnail path
|
|
||||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_path);
|
|
||||||
|
|
||||||
// if category exists, only include essential info
|
|
||||||
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
|
|
||||||
|
|
||||||
// modify duration
|
|
||||||
if (cropFileSettings) {
|
|
||||||
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user_uid) file_object['user_uid'] = user_uid;
|
|
||||||
if (sub_id) file_object['sub_id'] = sub_id;
|
|
||||||
|
|
||||||
const file_obj = await registerFileDBManual(file_object);
|
|
||||||
|
|
||||||
// remove metadata JSON if needed
|
|
||||||
if (!config_api.getConfigItem('ytdl_include_metadata')) {
|
|
||||||
utils.deleteJSONFile(file_path, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
return file_obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerFileDBManual(file_object) {
|
|
||||||
// add additional info
|
|
||||||
file_object['uid'] = uuid();
|
|
||||||
file_object['registered'] = Date.now();
|
|
||||||
path_object = path.parse(file_object['path']);
|
|
||||||
file_object['path'] = path.format(path_object);
|
|
||||||
|
|
||||||
await exports.insertRecordIntoTable('files', file_object, {path: file_object['path']})
|
|
||||||
|
|
||||||
return file_object;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateFileObject(file_path, type) {
|
|
||||||
var jsonobj = utils.getJSON(file_path, type);
|
|
||||||
if (!jsonobj) {
|
|
||||||
return null;
|
|
||||||
} else if (!jsonobj['_filename']) {
|
|
||||||
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const ext = (type === 'audio') ? '.mp3' : '.mp4'
|
|
||||||
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
|
|
||||||
// console.
|
|
||||||
var stats = fs.statSync(true_file_path);
|
|
||||||
|
|
||||||
const file_id = utils.removeFileExtension(path.basename(file_path));
|
|
||||||
var title = jsonobj.title;
|
|
||||||
var url = jsonobj.webpage_url;
|
|
||||||
var uploader = jsonobj.uploader;
|
|
||||||
var upload_date = utils.formatDateString(jsonobj.upload_date);
|
|
||||||
|
|
||||||
var size = stats.size;
|
|
||||||
|
|
||||||
var thumbnail = jsonobj.thumbnail;
|
|
||||||
var duration = jsonobj.duration;
|
|
||||||
var isaudio = type === 'audio';
|
|
||||||
var description = jsonobj.description;
|
|
||||||
var file_obj = new utils.File(file_id, title, thumbnail, isaudio, duration, url, uploader, size, true_file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
|
||||||
return file_obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAppendedBasePathSub(sub, base_path) {
|
|
||||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getFileDirectoriesAndDBs = async () => {
|
exports.getFileDirectoriesAndDBs = async () => {
|
||||||
@@ -317,277 +244,6 @@ exports.getFileDirectoriesAndDBs = async () => {
|
|||||||
return dirs_to_check;
|
return dirs_to_check;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.importUnregisteredFiles = async () => {
|
|
||||||
const imported_files = [];
|
|
||||||
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
|
|
||||||
|
|
||||||
// run through check list and check each file to see if it's missing from the db
|
|
||||||
for (let i = 0; i < dirs_to_check.length; i++) {
|
|
||||||
const dir_to_check = dirs_to_check[i];
|
|
||||||
// recursively get all files in dir's path
|
|
||||||
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
|
|
||||||
|
|
||||||
for (let j = 0; j < files.length; j++) {
|
|
||||||
const file = files[j];
|
|
||||||
|
|
||||||
// check if file exists in db, if not add it
|
|
||||||
const files_with_same_url = await exports.getRecords('files', {url: file.url, sub_id: dir_to_check.sub_id});
|
|
||||||
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
|
|
||||||
if (!file_is_registered) {
|
|
||||||
// add additional info
|
|
||||||
const file_obj = await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
|
|
||||||
if (file_obj) {
|
|
||||||
imported_files.push(file_obj['uid']);
|
|
||||||
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
|
||||||
} else {
|
|
||||||
logger.error(`Failed to import ${file['path']} automatically.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return imported_files;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.addMetadataPropertyToDB = async (property_key) => {
|
|
||||||
try {
|
|
||||||
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
|
|
||||||
const update_obj = {};
|
|
||||||
for (let i = 0; i < dirs_to_check.length; i++) {
|
|
||||||
const dir_to_check = dirs_to_check[i];
|
|
||||||
|
|
||||||
// recursively get all files in dir's path
|
|
||||||
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
|
|
||||||
for (let j = 0; j < files.length; j++) {
|
|
||||||
const file = files[j];
|
|
||||||
if (file[property_key]) {
|
|
||||||
update_obj[file.uid] = {[property_key]: file[property_key]};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await exports.bulkUpdateRecordsByKey('files', 'uid', update_obj);
|
|
||||||
} catch(err) {
|
|
||||||
logger.error(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
|
|
||||||
const first_video = await exports.getVideo(uids[0]);
|
|
||||||
const thumbnailToUse = first_video['thumbnailURL'];
|
|
||||||
|
|
||||||
let new_playlist = {
|
|
||||||
name: playlist_name,
|
|
||||||
uids: uids,
|
|
||||||
id: uuid(),
|
|
||||||
thumbnailURL: thumbnailToUse,
|
|
||||||
registered: Date.now(),
|
|
||||||
randomize_order: false
|
|
||||||
};
|
|
||||||
|
|
||||||
new_playlist.user_uid = user_uid ? user_uid : undefined;
|
|
||||||
|
|
||||||
await exports.insertRecordIntoTable('playlists', new_playlist);
|
|
||||||
|
|
||||||
const duration = await exports.calculatePlaylistDuration(new_playlist);
|
|
||||||
await exports.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
|
|
||||||
|
|
||||||
return new_playlist;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => {
|
|
||||||
let playlist = await exports.getRecord('playlists', {id: playlist_id});
|
|
||||||
|
|
||||||
if (!playlist) {
|
|
||||||
playlist = await exports.getRecord('categories', {uid: playlist_id});
|
|
||||||
if (playlist) {
|
|
||||||
const uids = (await exports.getRecords('files', {'category.uid': playlist_id})).map(file => file.uid);
|
|
||||||
playlist['uids'] = uids;
|
|
||||||
playlist['auto'] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// converts playlists to new UID-based schema
|
|
||||||
if (playlist && playlist['fileNames'] && !playlist['uids']) {
|
|
||||||
playlist['uids'] = [];
|
|
||||||
logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`);
|
|
||||||
for (let i = 0; i < playlist['fileNames'].length; i++) {
|
|
||||||
const fileName = playlist['fileNames'][i];
|
|
||||||
const uid = await exports.getVideoUIDByID(fileName, user_uid);
|
|
||||||
if (uid) playlist['uids'].push(uid);
|
|
||||||
else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`);
|
|
||||||
}
|
|
||||||
exports.updatePlaylist(playlist, user_uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent unauthorized users from accessing the file info
|
|
||||||
if (require_sharing && !playlist['sharingEnabled']) return null;
|
|
||||||
|
|
||||||
return playlist;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updatePlaylist = async (playlist) => {
|
|
||||||
let playlistID = playlist.id;
|
|
||||||
|
|
||||||
const duration = await exports.calculatePlaylistDuration(playlist);
|
|
||||||
playlist.duration = duration;
|
|
||||||
|
|
||||||
return await exports.updateRecord('playlists', {id: playlistID}, playlist);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = null) => {
|
|
||||||
let success = await exports.updateRecord('playlists', {id: playlist_id}, assignment_obj);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
success = await exports.updateRecord('categories', {uid: playlist_id}, assignment_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
logger.error(`Could not find playlist or category with ID ${playlist_id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
|
|
||||||
if (!playlist_file_objs) {
|
|
||||||
playlist_file_objs = [];
|
|
||||||
for (let i = 0; i < playlist['uids'].length; i++) {
|
|
||||||
const uid = playlist['uids'][i];
|
|
||||||
const file_obj = await exports.getVideo(uid);
|
|
||||||
if (file_obj) playlist_file_objs.push(file_obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.deleteFile = async (uid, blacklistMode = false) => {
|
|
||||||
const file_obj = await exports.getVideo(uid);
|
|
||||||
const type = file_obj.isAudio ? 'audio' : 'video';
|
|
||||||
const folderPath = path.dirname(file_obj.path);
|
|
||||||
const ext = type === 'audio' ? 'mp3' : 'mp4';
|
|
||||||
const name = file_obj.id;
|
|
||||||
const filePathNoExtension = utils.removeFileExtension(file_obj.path);
|
|
||||||
|
|
||||||
var jsonPath = `${file_obj.path}.info.json`;
|
|
||||||
var altJSONPath = `${filePathNoExtension}.info.json`;
|
|
||||||
var thumbnailPath = `${filePathNoExtension}.webp`;
|
|
||||||
var altThumbnailPath = `${filePathNoExtension}.jpg`;
|
|
||||||
|
|
||||||
jsonPath = path.join(__dirname, jsonPath);
|
|
||||||
altJSONPath = path.join(__dirname, altJSONPath);
|
|
||||||
|
|
||||||
let jsonExists = await fs.pathExists(jsonPath);
|
|
||||||
let thumbnailExists = await fs.pathExists(thumbnailPath);
|
|
||||||
|
|
||||||
if (!jsonExists) {
|
|
||||||
if (await fs.pathExists(altJSONPath)) {
|
|
||||||
jsonExists = true;
|
|
||||||
jsonPath = altJSONPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!thumbnailExists) {
|
|
||||||
if (await fs.pathExists(altThumbnailPath)) {
|
|
||||||
thumbnailExists = true;
|
|
||||||
thumbnailPath = altThumbnailPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileExists = await fs.pathExists(file_obj.path);
|
|
||||||
|
|
||||||
if (config_api.descriptors[uid]) {
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < config_api.descriptors[uid].length; i++) {
|
|
||||||
config_api.descriptors[uid][i].destroy();
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
|
||||||
if (useYoutubeDLArchive) {
|
|
||||||
// get id/extractor from JSON
|
|
||||||
|
|
||||||
const info_json = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath));
|
|
||||||
let retrievedID = null;
|
|
||||||
let retrievedExtractor = null;
|
|
||||||
if (info_json) {
|
|
||||||
retrievedID = info_json['id'];
|
|
||||||
retrievedExtractor = info_json['extractor'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
|
|
||||||
if (!blacklistMode) {
|
|
||||||
// workaround until a files_api is created (using archive_api would make a circular dependency)
|
|
||||||
await exports.removeAllRecords('archives', {extractor: retrievedExtractor, id: retrievedID, type: type, user_uid: file_obj.user_uid, sub_id: file_obj.sub_id});
|
|
||||||
// await archive_api.removeFromArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonExists) await fs.unlink(jsonPath);
|
|
||||||
if (thumbnailExists) await fs.unlink(thumbnailPath);
|
|
||||||
|
|
||||||
await exports.removeRecord('files', {uid: uid});
|
|
||||||
|
|
||||||
if (fileExists) {
|
|
||||||
await fs.unlink(file_obj.path);
|
|
||||||
if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO: tell user that the file didn't exist
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
|
|
||||||
exports.getVideoUIDByID = async (file_id, uuid = null) => {
|
|
||||||
const file_obj = await exports.getRecord('files', {id: file_id});
|
|
||||||
return file_obj ? file_obj['uid'] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getVideo = async (file_uid) => {
|
|
||||||
return await exports.getRecord('files', {uid: file_uid});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getAllFiles = async (sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid) => {
|
|
||||||
const filter_obj = {user_uid: uuid};
|
|
||||||
const regex = true;
|
|
||||||
if (text_search) {
|
|
||||||
if (regex) {
|
|
||||||
filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
|
|
||||||
} else {
|
|
||||||
filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (favorite_filter) {
|
|
||||||
filter_obj['favorite'] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub_id) {
|
|
||||||
filter_obj['sub_id'] = sub_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
|
|
||||||
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
|
|
||||||
|
|
||||||
const files = JSON.parse(JSON.stringify(await exports.getRecords('files', filter_obj, false, sort, range, text_search)));
|
|
||||||
const file_count = await exports.getRecords('files', filter_obj, true);
|
|
||||||
|
|
||||||
return {files, file_count};
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setVideoProperty = async (file_uid, assignment_obj) => {
|
|
||||||
// TODO: check if video exists, throw error if not
|
|
||||||
await exports.updateRecord('files', {uid: file_uid}, assignment_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic DB functions
|
// Basic DB functions
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
@@ -698,9 +354,15 @@ exports.getRecords = async (table, filter_obj = null, return_count = false, sort
|
|||||||
|
|
||||||
// Update
|
// Update
|
||||||
|
|
||||||
exports.updateRecord = async (table, filter_obj, update_obj) => {
|
exports.updateRecord = async (table, filter_obj, update_obj, nested_mode = false) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
|
if (nested_mode) {
|
||||||
|
// if object is nested we need to handle it differently
|
||||||
|
update_obj = utils.convertFlatObjectToNestedObject(update_obj);
|
||||||
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').merge(update_obj).write();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write();
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -714,7 +376,14 @@ exports.updateRecord = async (table, filter_obj, update_obj) => {
|
|||||||
exports.updateRecords = async (table, filter_obj, update_obj) => {
|
exports.updateRecords = async (table, filter_obj, update_obj) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write();
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').each((record) => {
|
||||||
|
const props_to_update = Object.keys(update_obj);
|
||||||
|
for (let i = 0; i < props_to_update.length; i++) {
|
||||||
|
const prop_to_update = props_to_update[i];
|
||||||
|
const prop_value = update_obj[prop_to_update];
|
||||||
|
record[prop_to_update] = prop_value;
|
||||||
|
}
|
||||||
|
}).write();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,6 +391,18 @@ exports.updateRecords = async (table, filter_obj, update_obj) => {
|
|||||||
return !!(output['result']['ok']);
|
return !!(output['result']['ok']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.removePropertyFromRecord = async (table, filter_obj, remove_obj) => {
|
||||||
|
// local db override
|
||||||
|
if (using_local_db) {
|
||||||
|
const props_to_remove = Object.keys(remove_obj);
|
||||||
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').unset(props_to_remove).write();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = await database.collection(table).updateOne(filter_obj, {$unset: remove_obj});
|
||||||
|
return !!(output['result']['ok']);
|
||||||
|
}
|
||||||
|
|
||||||
exports.bulkUpdateRecordsByKey = async (table, key_label, update_obj) => {
|
exports.bulkUpdateRecordsByKey = async (table, key_label, update_obj) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const { create } = require('xmlbuilder2');
|
|||||||
const categories_api = require('./categories');
|
const categories_api = require('./categories');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
|
const files_api = require('./files');
|
||||||
const notifications_api = require('./notifications');
|
const notifications_api = require('./notifications');
|
||||||
const archive_api = require('./archive');
|
const archive_api = require('./archive');
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ exports.clearDownload = async (download_uid) => {
|
|||||||
|
|
||||||
async function handleDownloadError(download, error_message, error_type = null) {
|
async function handleDownloadError(download, error_message, error_type = null) {
|
||||||
if (!download || !download['uid']) return;
|
if (!download || !download['uid']) return;
|
||||||
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_type);
|
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_message, error_type);
|
||||||
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
|
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +222,7 @@ async function collectInfo(download_uid) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// in subscriptions we don't care if archive mode is enabled, but we already removed archived videos from subs by this point
|
||||||
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||||
if (useYoutubeDLArchive && !options.ignoreArchive) {
|
if (useYoutubeDLArchive && !options.ignoreArchive) {
|
||||||
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info['id'], type, download['user_uid'], download['sub_id']);
|
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info['id'], type, download['user_uid'], download['sub_id']);
|
||||||
@@ -245,11 +247,10 @@ async function collectInfo(download_uid) {
|
|||||||
options.customOutput = category['custom_output'];
|
options.customOutput = category['custom_output'];
|
||||||
options.noRelativePath = true;
|
options.noRelativePath = true;
|
||||||
args = await exports.generateArgs(url, type, options, download['user_uid']);
|
args = await exports.generateArgs(url, type, options, download['user_uid']);
|
||||||
args = utils.filterArgs(args, ['--no-simulate']);
|
|
||||||
info = await exports.getVideoInfoByURL(url, args, download_uid);
|
info = await exports.getVideoInfoByURL(url, args, download_uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
download['category'] = category;
|
const stripped_category = category ? {name: category['name'], uid: category['uid']} : null;
|
||||||
|
|
||||||
// setup info required to calculate download progress
|
// setup info required to calculate download progress
|
||||||
|
|
||||||
@@ -272,6 +273,7 @@ async function collectInfo(download_uid) {
|
|||||||
files_to_check_for_progress: files_to_check_for_progress,
|
files_to_check_for_progress: files_to_check_for_progress,
|
||||||
expected_file_size: expected_file_size,
|
expected_file_size: expected_file_size,
|
||||||
title: playlist_title ? playlist_title : info['title'],
|
title: playlist_title ? playlist_title : info['title'],
|
||||||
|
category: stripped_category,
|
||||||
prefetched_info: null
|
prefetched_info: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -314,7 +316,7 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
clearInterval(download_checker);
|
clearInterval(download_checker);
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error(err.stderr);
|
logger.error(err.stderr);
|
||||||
await handleDownloadError(download, err.stderr);
|
await handleDownloadError(download, err.stderr, 'unknown_error');
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
} else if (output) {
|
} else if (output) {
|
||||||
@@ -350,7 +352,7 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
||||||
|
|
||||||
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||||
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
&& config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||||
let vodId = url.split('twitch.tv/videos/')[1];
|
let vodId = url.split('twitch.tv/videos/')[1];
|
||||||
vodId = vodId.split('?')[0];
|
vodId = vodId.split('?')[0];
|
||||||
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
||||||
@@ -384,10 +386,9 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// registers file in DB
|
// registers file in DB
|
||||||
const file_obj = await db_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
|
const file_obj = await files_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
|
||||||
|
|
||||||
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
|
||||||
if (useYoutubeDLArchive && !options.ignoreArchive) await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
|
|
||||||
|
|
||||||
notifications_api.sendDownloadNotification(file_obj, download['user_uid']);
|
notifications_api.sendDownloadNotification(file_obj, download['user_uid']);
|
||||||
|
|
||||||
@@ -399,7 +400,7 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
if (file_objs.length > 1) {
|
if (file_objs.length > 1) {
|
||||||
// create playlist
|
// create playlist
|
||||||
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
|
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
|
||||||
container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
|
container = await files_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
|
||||||
} else if (file_objs.length === 1) {
|
} else if (file_objs.length === 1) {
|
||||||
container = file_objs[0];
|
container = file_objs[0];
|
||||||
} else {
|
} else {
|
||||||
@@ -552,7 +553,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
// remove bad args
|
// remove bad args
|
||||||
const new_args = [...args];
|
const temp_args = utils.filterArgs(args, ['--no-simulate']);
|
||||||
|
const new_args = [...temp_args];
|
||||||
|
|
||||||
const archiveArgIndex = new_args.indexOf('--download-archive');
|
const archiveArgIndex = new_args.indexOf('--download-archive');
|
||||||
if (archiveArgIndex !== -1) {
|
if (archiveArgIndex !== -1) {
|
||||||
@@ -595,7 +597,7 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
|||||||
logger.error(error_message);
|
logger.error(error_message);
|
||||||
if (download_uid) {
|
if (download_uid) {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
await handleDownloadError(download, error_message);
|
await handleDownloadError(download, error_message, 'info_retrieve_failed');
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
350
backend/files.js
Normal file
350
backend/files.js
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
const fs = require('fs-extra')
|
||||||
|
const path = require('path')
|
||||||
|
const { uuid } = require('uuidv4');
|
||||||
|
|
||||||
|
const config_api = require('./config');
|
||||||
|
const db_api = require('./db');
|
||||||
|
const archive_api = require('./archive');
|
||||||
|
const utils = require('./utils')
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
exports.registerFileDB = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
|
||||||
|
if (!file_object) file_object = generateFileObject(file_path, type);
|
||||||
|
if (!file_object) {
|
||||||
|
logger.error(`Could not find associated JSON file for ${type} file ${file_path}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.fixVideoMetadataPerms(file_path, type);
|
||||||
|
|
||||||
|
// add thumbnail path
|
||||||
|
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_path);
|
||||||
|
|
||||||
|
// if category exists, only include essential info
|
||||||
|
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
|
||||||
|
|
||||||
|
// modify duration
|
||||||
|
if (cropFileSettings) {
|
||||||
|
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_uid) file_object['user_uid'] = user_uid;
|
||||||
|
if (sub_id) file_object['sub_id'] = sub_id;
|
||||||
|
|
||||||
|
const file_obj = await registerFileDBManual(file_object);
|
||||||
|
|
||||||
|
// remove metadata JSON if needed
|
||||||
|
if (!config_api.getConfigItem('ytdl_include_metadata')) {
|
||||||
|
utils.deleteJSONFile(file_path, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerFileDBManual(file_object) {
|
||||||
|
// add additional info
|
||||||
|
file_object['uid'] = uuid();
|
||||||
|
file_object['registered'] = Date.now();
|
||||||
|
const path_object = path.parse(file_object['path']);
|
||||||
|
file_object['path'] = path.format(path_object);
|
||||||
|
|
||||||
|
await db_api.insertRecordIntoTable('files', file_object, {path: file_object['path']})
|
||||||
|
|
||||||
|
return file_object;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateFileObject(file_path, type) {
|
||||||
|
const jsonobj = utils.getJSON(file_path, type);
|
||||||
|
if (!jsonobj) {
|
||||||
|
return null;
|
||||||
|
} else if (!jsonobj['_filename']) {
|
||||||
|
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
|
||||||
|
// console.
|
||||||
|
const stats = fs.statSync(true_file_path);
|
||||||
|
|
||||||
|
const file_id = utils.removeFileExtension(path.basename(file_path));
|
||||||
|
const title = jsonobj.title;
|
||||||
|
const url = jsonobj.webpage_url;
|
||||||
|
const uploader = jsonobj.uploader;
|
||||||
|
const upload_date = utils.formatDateString(jsonobj.upload_date);
|
||||||
|
|
||||||
|
const size = stats.size;
|
||||||
|
|
||||||
|
const thumbnail = jsonobj.thumbnail;
|
||||||
|
const duration = jsonobj.duration;
|
||||||
|
const isaudio = type === 'audio';
|
||||||
|
const description = jsonobj.description;
|
||||||
|
const file_obj = new utils.File(file_id, title, thumbnail, isaudio, duration, url, uploader, size, true_file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
||||||
|
return file_obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.importUnregisteredFiles = async () => {
|
||||||
|
const imported_files = [];
|
||||||
|
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
|
||||||
|
|
||||||
|
// run through check list and check each file to see if it's missing from the db
|
||||||
|
for (let i = 0; i < dirs_to_check.length; i++) {
|
||||||
|
const dir_to_check = dirs_to_check[i];
|
||||||
|
// recursively get all files in dir's path
|
||||||
|
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
|
||||||
|
|
||||||
|
for (let j = 0; j < files.length; j++) {
|
||||||
|
const file = files[j];
|
||||||
|
|
||||||
|
// check if file exists in db, if not add it
|
||||||
|
const files_with_same_url = await db_api.getRecords('files', {url: file.url, sub_id: dir_to_check.sub_id});
|
||||||
|
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
|
||||||
|
if (!file_is_registered) {
|
||||||
|
// add additional info
|
||||||
|
const file_obj = await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
|
||||||
|
if (file_obj) {
|
||||||
|
imported_files.push(file_obj['uid']);
|
||||||
|
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
||||||
|
} else {
|
||||||
|
logger.error(`Failed to import ${file['path']} automatically.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imported_files;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.addMetadataPropertyToDB = async (property_key) => {
|
||||||
|
try {
|
||||||
|
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
|
||||||
|
const update_obj = {};
|
||||||
|
for (let i = 0; i < dirs_to_check.length; i++) {
|
||||||
|
const dir_to_check = dirs_to_check[i];
|
||||||
|
|
||||||
|
// recursively get all files in dir's path
|
||||||
|
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
|
||||||
|
for (let j = 0; j < files.length; j++) {
|
||||||
|
const file = files[j];
|
||||||
|
if (file[property_key]) {
|
||||||
|
update_obj[file.uid] = {[property_key]: file[property_key]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await db_api.bulkUpdateRecordsByKey('files', 'uid', update_obj);
|
||||||
|
} catch(err) {
|
||||||
|
logger.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
|
||||||
|
const first_video = await exports.getVideo(uids[0]);
|
||||||
|
const thumbnailToUse = first_video['thumbnailURL'];
|
||||||
|
|
||||||
|
let new_playlist = {
|
||||||
|
name: playlist_name,
|
||||||
|
uids: uids,
|
||||||
|
id: uuid(),
|
||||||
|
thumbnailURL: thumbnailToUse,
|
||||||
|
registered: Date.now(),
|
||||||
|
randomize_order: false
|
||||||
|
};
|
||||||
|
|
||||||
|
new_playlist.user_uid = user_uid ? user_uid : undefined;
|
||||||
|
|
||||||
|
await db_api.insertRecordIntoTable('playlists', new_playlist);
|
||||||
|
|
||||||
|
const duration = await exports.calculatePlaylistDuration(new_playlist);
|
||||||
|
await db_api.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
|
||||||
|
|
||||||
|
return new_playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => {
|
||||||
|
let playlist = await db_api.getRecord('playlists', {id: playlist_id});
|
||||||
|
|
||||||
|
if (!playlist) {
|
||||||
|
playlist = await db_api.getRecord('categories', {uid: playlist_id});
|
||||||
|
if (playlist) {
|
||||||
|
const uids = (await db_api.getRecords('files', {'category.uid': playlist_id})).map(file => file.uid);
|
||||||
|
playlist['uids'] = uids;
|
||||||
|
playlist['auto'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// converts playlists to new UID-based schema
|
||||||
|
if (playlist && playlist['fileNames'] && !playlist['uids']) {
|
||||||
|
playlist['uids'] = [];
|
||||||
|
logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`);
|
||||||
|
for (let i = 0; i < playlist['fileNames'].length; i++) {
|
||||||
|
const fileName = playlist['fileNames'][i];
|
||||||
|
const uid = await exports.getVideoUIDByID(fileName, user_uid);
|
||||||
|
if (uid) playlist['uids'].push(uid);
|
||||||
|
else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`);
|
||||||
|
}
|
||||||
|
exports.updatePlaylist(playlist, user_uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent unauthorized users from accessing the file info
|
||||||
|
if (require_sharing && !playlist['sharingEnabled']) return null;
|
||||||
|
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updatePlaylist = async (playlist) => {
|
||||||
|
let playlistID = playlist.id;
|
||||||
|
|
||||||
|
const duration = await exports.calculatePlaylistDuration(playlist);
|
||||||
|
playlist.duration = duration;
|
||||||
|
|
||||||
|
return await db_api.updateRecord('playlists', {id: playlistID}, playlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = null) => {
|
||||||
|
let success = await db_api.updateRecord('playlists', {id: playlist_id}, assignment_obj);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
success = await db_api.updateRecord('categories', {uid: playlist_id}, assignment_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
logger.error(`Could not find playlist or category with ID ${playlist_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
|
||||||
|
if (!playlist_file_objs) {
|
||||||
|
playlist_file_objs = [];
|
||||||
|
for (let i = 0; i < playlist['uids'].length; i++) {
|
||||||
|
const uid = playlist['uids'][i];
|
||||||
|
const file_obj = await exports.getVideo(uid);
|
||||||
|
if (file_obj) playlist_file_objs.push(file_obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.deleteFile = async (uid, blacklistMode = false) => {
|
||||||
|
const file_obj = await exports.getVideo(uid);
|
||||||
|
const type = file_obj.isAudio ? 'audio' : 'video';
|
||||||
|
const folderPath = path.dirname(file_obj.path);
|
||||||
|
const name = file_obj.id;
|
||||||
|
const filePathNoExtension = utils.removeFileExtension(file_obj.path);
|
||||||
|
|
||||||
|
var jsonPath = `${file_obj.path}.info.json`;
|
||||||
|
var altJSONPath = `${filePathNoExtension}.info.json`;
|
||||||
|
var thumbnailPath = `${filePathNoExtension}.webp`;
|
||||||
|
var altThumbnailPath = `${filePathNoExtension}.jpg`;
|
||||||
|
|
||||||
|
jsonPath = path.join(__dirname, jsonPath);
|
||||||
|
altJSONPath = path.join(__dirname, altJSONPath);
|
||||||
|
|
||||||
|
let jsonExists = await fs.pathExists(jsonPath);
|
||||||
|
let thumbnailExists = await fs.pathExists(thumbnailPath);
|
||||||
|
|
||||||
|
if (!jsonExists) {
|
||||||
|
if (await fs.pathExists(altJSONPath)) {
|
||||||
|
jsonExists = true;
|
||||||
|
jsonPath = altJSONPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!thumbnailExists) {
|
||||||
|
if (await fs.pathExists(altThumbnailPath)) {
|
||||||
|
thumbnailExists = true;
|
||||||
|
thumbnailPath = altThumbnailPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileExists = await fs.pathExists(file_obj.path);
|
||||||
|
|
||||||
|
if (config_api.descriptors[uid]) {
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < config_api.descriptors[uid].length; i++) {
|
||||||
|
config_api.descriptors[uid][i].destroy();
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||||
|
if (useYoutubeDLArchive || file_obj.sub_id) {
|
||||||
|
// get id/extractor from JSON
|
||||||
|
|
||||||
|
const info_json = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath));
|
||||||
|
let retrievedID = null;
|
||||||
|
let retrievedExtractor = null;
|
||||||
|
if (info_json) {
|
||||||
|
retrievedID = info_json['id'];
|
||||||
|
retrievedExtractor = info_json['extractor'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
|
||||||
|
if (!blacklistMode) {
|
||||||
|
await archive_api.removeFromArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id)
|
||||||
|
} else {
|
||||||
|
const exists_in_archive = await archive_api.existsInArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id);
|
||||||
|
if (!exists_in_archive) {
|
||||||
|
await archive_api.addToArchive(retrievedExtractor, retrievedID, type, file_obj.title, file_obj.user_uid, file_obj.sub_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonExists) await fs.unlink(jsonPath);
|
||||||
|
if (thumbnailExists) await fs.unlink(thumbnailPath);
|
||||||
|
|
||||||
|
await db_api.removeRecord('files', {uid: uid});
|
||||||
|
|
||||||
|
if (fileExists) {
|
||||||
|
await fs.unlink(file_obj.path);
|
||||||
|
if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: tell user that the file didn't exist
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
|
||||||
|
exports.getVideoUIDByID = async (file_id, uuid = null) => {
|
||||||
|
const file_obj = await db_api.getRecord('files', {id: file_id});
|
||||||
|
return file_obj ? file_obj['uid'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getVideo = async (file_uid) => {
|
||||||
|
return await db_api.getRecord('files', {uid: file_uid});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getAllFiles = async (sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid) => {
|
||||||
|
const filter_obj = {user_uid: uuid};
|
||||||
|
const regex = true;
|
||||||
|
if (text_search) {
|
||||||
|
if (regex) {
|
||||||
|
filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
|
||||||
|
} else {
|
||||||
|
filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (favorite_filter) {
|
||||||
|
filter_obj['favorite'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub_id) {
|
||||||
|
filter_obj['sub_id'] = sub_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
|
||||||
|
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
|
||||||
|
|
||||||
|
const files = JSON.parse(JSON.stringify(await db_api.getRecords('files', filter_obj, false, sort, range, text_search)));
|
||||||
|
const file_count = await db_api.getRecords('files', filter_obj, true);
|
||||||
|
|
||||||
|
return {files, file_count};
|
||||||
|
}
|
||||||
@@ -2,12 +2,16 @@ const db_api = require('./db');
|
|||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
|
const consts = require('./consts');
|
||||||
|
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
|
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const { gotify } = require("gotify");
|
const { gotify } = require("gotify");
|
||||||
const TelegramBot = require('node-telegram-bot-api');
|
const TelegramBot = require('node-telegram-bot-api');
|
||||||
|
const REST = require('@discordjs/rest').REST;
|
||||||
|
const API = require('@discordjs/core').API;
|
||||||
|
const EmbedBuilder = require('@discordjs/builders').EmbedBuilder;
|
||||||
|
|
||||||
const NOTIFICATION_TYPE_TO_TITLE = {
|
const NOTIFICATION_TYPE_TO_TITLE = {
|
||||||
task_finished: 'Task finished',
|
task_finished: 'Task finished',
|
||||||
@@ -18,7 +22,7 @@ const NOTIFICATION_TYPE_TO_TITLE = {
|
|||||||
const NOTIFICATION_TYPE_TO_BODY = {
|
const NOTIFICATION_TYPE_TO_BODY = {
|
||||||
task_finished: (notification) => notification['data']['task_title'],
|
task_finished: (notification) => notification['data']['task_title'],
|
||||||
download_complete: (notification) => {return `${notification['data']['file_title']}\nOriginal URL: ${notification['data']['original_url']}`},
|
download_complete: (notification) => {return `${notification['data']['file_title']}\nOriginal URL: ${notification['data']['original_url']}`},
|
||||||
download_error: (notification) => {return `Error: ${notification['data']['download_error_type']}\nURL: ${notification['data']['download_url']}`}
|
download_error: (notification) => {return `Error: ${notification['data']['download_error_message']}\nError code: ${notification['data']['download_error_type']}\n\nOriginal URL: ${notification['data']['download_url']}`}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOTIFICATION_TYPE_TO_URL = {
|
const NOTIFICATION_TYPE_TO_URL = {
|
||||||
@@ -57,6 +61,12 @@ exports.sendNotification = async (notification) => {
|
|||||||
if (config_api.getConfigItem('ytdl_webhook_url')) {
|
if (config_api.getConfigItem('ytdl_webhook_url')) {
|
||||||
sendGenericNotification(data);
|
sendGenericNotification(data);
|
||||||
}
|
}
|
||||||
|
if (config_api.getConfigItem('ytdl_discord_webhook_url')) {
|
||||||
|
sendDiscordNotification(data);
|
||||||
|
}
|
||||||
|
if (config_api.getConfigItem('ytdl_slack_webhook_url')) {
|
||||||
|
sendSlackNotification(data);
|
||||||
|
}
|
||||||
|
|
||||||
await db_api.insertRecordIntoTable('notifications', notification);
|
await db_api.insertRecordIntoTable('notifications', notification);
|
||||||
return notification;
|
return notification;
|
||||||
@@ -79,9 +89,9 @@ exports.sendDownloadNotification = async (file, user_uid) => {
|
|||||||
return await exports.sendNotification(notification);
|
return await exports.sendNotification(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.sendDownloadErrorNotification = async (download, user_uid, error_type = null) => {
|
exports.sendDownloadErrorNotification = async (download, user_uid, error_message, error_type = null) => {
|
||||||
if (!notificationEnabled('download_error')) return;
|
if (!notificationEnabled('download_error')) return;
|
||||||
const data = {download_uid: download.uid, download_url: download.url, download_error_type: error_type};
|
const data = {download_uid: download.uid, download_url: download.url, download_error_message: error_message, download_error_type: error_type};
|
||||||
const notification = exports.createNotification('download_error', ['view_download_error', 'retry_download'], data, user_uid);
|
const notification = exports.createNotification('download_error', ['view_download_error', 'retry_download'], data, user_uid);
|
||||||
return await exports.sendNotification(notification);
|
return await exports.sendNotification(notification);
|
||||||
}
|
}
|
||||||
@@ -144,6 +154,88 @@ async function sendTelegramNotification({body, title, type, url, thumbnail}) {
|
|||||||
bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
|
bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
||||||
|
const discord_webhook_url = config_api.getConfigItem('ytdl_discord_webhook_url');
|
||||||
|
const url_split = discord_webhook_url.split('webhooks/');
|
||||||
|
const [webhook_id, webhook_token] = url_split[1].split('/');
|
||||||
|
const rest = new REST({ version: '10' });
|
||||||
|
const api = new API(rest);
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(title)
|
||||||
|
.setColor(0x00FFFF)
|
||||||
|
.setURL(url)
|
||||||
|
.setDescription(`ID: ${type}`);
|
||||||
|
if (thumbnail) embed.setThumbnail(thumbnail);
|
||||||
|
if (type === 'download_error') embed.setColor(0xFC2003);
|
||||||
|
|
||||||
|
const result = await api.webhooks.execute(webhook_id, webhook_token, {
|
||||||
|
content: body,
|
||||||
|
username: 'YoutubeDL-Material',
|
||||||
|
avatar_url: consts.ICON_URL,
|
||||||
|
embeds: [embed],
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendSlackNotification({body, title, type, url, thumbnail}) {
|
||||||
|
const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url');
|
||||||
|
logger.verbose(`Sending slack notification to ${slack_webhook_url}`);
|
||||||
|
const data = {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "section",
|
||||||
|
text: {
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `*${title}*`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "section",
|
||||||
|
text: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// add thumbnail if exists
|
||||||
|
if (thumbnail) {
|
||||||
|
data['blocks'].push({
|
||||||
|
type: "image",
|
||||||
|
image_url: thumbnail,
|
||||||
|
alt_text: "notification_thumbnail"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
data['blocks'].push(
|
||||||
|
{
|
||||||
|
type: "section",
|
||||||
|
text: {
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `<${url}|${url}>`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "context",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `*ID:* ${type}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
fetch(slack_webhook_url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function sendGenericNotification(data) {
|
function sendGenericNotification(data) {
|
||||||
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
|
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
|
||||||
logger.verbose(`Sending generic notification to ${webhook_url}`);
|
logger.verbose(`Sending generic notification to ${webhook_url}`);
|
||||||
|
|||||||
556
backend/package-lock.json
generated
556
backend/package-lock.json
generated
@@ -19,6 +19,101 @@
|
|||||||
"kuler": "^2.0.0"
|
"kuler": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@discordjs/builders": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-CCcLwn/8ANhlAbhlE18fcaN0hfXTen53/JiwZs1t9oE/Cqa9maA8ZRarkCIsXF4J7J/MYnd0J6IsxeKsq+f6mw==",
|
||||||
|
"requires": {
|
||||||
|
"@discordjs/formatters": "^0.3.0",
|
||||||
|
"@discordjs/util": "^0.2.0",
|
||||||
|
"@sapphire/shapeshift": "^3.8.1",
|
||||||
|
"discord-api-types": "^0.37.37",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"ts-mixer": "^6.0.3",
|
||||||
|
"tslib": "^2.5.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@discordjs/collection": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-suyVndkEAAWrGxyw/CPGdtXoRRU6AUNkibtnbJevQzpelkJh3Q1gQqWDpqf5i39CnAn5+LrN0YS+cULeEjq2Yw=="
|
||||||
|
},
|
||||||
|
"@discordjs/core": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/core/-/core-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-OEgK8GYNB1IJK3nPQ3QBvNUmuvlPTitc0j9oXe801Z7xWOFwL/lePAGhd6cAFH7yYaslwhCoSh85KI9glrmjNQ==",
|
||||||
|
"requires": {
|
||||||
|
"@discordjs/rest": "^1.7.0",
|
||||||
|
"@discordjs/util": "^0.2.0",
|
||||||
|
"@discordjs/ws": "^0.8.1",
|
||||||
|
"@sapphire/snowflake": "^3.4.2",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.2.1",
|
||||||
|
"discord-api-types": "^0.37.38"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@discordjs/formatters": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-Fc4MomalbP8HMKEMor3qUiboAKDtR7PSBoPjwm7WYghVRwgJlj5WYvUsriLsxeKk8+Qq2oy+HJlGTUkGvX0YnA==",
|
||||||
|
"requires": {
|
||||||
|
"discord-api-types": "^0.37.37"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@discordjs/rest": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-r2HzmznRIo8IDGYBWqQfkEaGN1LrFfWQd3dSyC4tOpMU8nuVvFUEw6V/lwnG44jyOq+vgyDny2fxeUDMt9I4aQ==",
|
||||||
|
"requires": {
|
||||||
|
"@discordjs/collection": "^1.5.0",
|
||||||
|
"@discordjs/util": "^0.2.0",
|
||||||
|
"@sapphire/async-queue": "^1.5.0",
|
||||||
|
"@sapphire/snowflake": "^3.4.0",
|
||||||
|
"discord-api-types": "^0.37.37",
|
||||||
|
"file-type": "^18.2.1",
|
||||||
|
"tslib": "^2.5.0",
|
||||||
|
"undici": "^5.21.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"file-type": {
|
||||||
|
"version": "18.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-type/-/file-type-18.3.0.tgz",
|
||||||
|
"integrity": "sha512-pkPZ5OGIq0TYb37b8bHDLNeQSe1H2KlaQ2ySGpJkkr2KZdaWsO4QhPzHA0mQcsUW2cSqJk+4gM/UyLz/UFbXdQ==",
|
||||||
|
"requires": {
|
||||||
|
"readable-web-to-node-stream": "^3.0.2",
|
||||||
|
"strtok3": "^7.0.0",
|
||||||
|
"token-types": "^5.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@discordjs/util": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-/8qNbebFzLWKOOg+UV+RB8itp4SmU5jw0tBUD3ifElW6rYNOj1Ku5JaSW7lLl/WgjjxF01l/1uQPCzkwr110vg=="
|
||||||
|
},
|
||||||
|
"@discordjs/ws": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-RZwlluBGmrgAgvTHP8w9IW7Kp/idWEQgSHBs5h0ecqiWGVCueoIr6jMmvbxqZ7vVirric3zRhNdmG/TNRxhWLg==",
|
||||||
|
"requires": {
|
||||||
|
"@discordjs/collection": "^1.5.0",
|
||||||
|
"@discordjs/rest": "^1.7.0",
|
||||||
|
"@discordjs/util": "^0.2.0",
|
||||||
|
"@sapphire/async-queue": "^1.5.0",
|
||||||
|
"@types/ws": "^8.5.4",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.2.1",
|
||||||
|
"discord-api-types": "^0.37.38",
|
||||||
|
"tslib": "^2.5.0",
|
||||||
|
"ws": "^8.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@oozcitak/dom": {
|
"@oozcitak/dom": {
|
||||||
"version": "1.15.10",
|
"version": "1.15.10",
|
||||||
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
|
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
|
||||||
@@ -51,6 +146,32 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
|
||||||
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
|
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
|
||||||
},
|
},
|
||||||
|
"@sapphire/async-queue": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA=="
|
||||||
|
},
|
||||||
|
"@sapphire/shapeshift": {
|
||||||
|
"version": "3.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.8.2.tgz",
|
||||||
|
"integrity": "sha512-NXpnJAsxN3/h9TqQPntOeVWZrpIuucqXI3IWF6tj2fWCoRLCuVK5wx7Dtg7pRrtkYfsMUbDqgKoX26vrC5iYfA==",
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@sapphire/snowflake": {
|
||||||
|
"version": "3.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.4.2.tgz",
|
||||||
|
"integrity": "sha512-KJwlv5gkGjs1uFV7/xx81n3tqgBwBJvH94n1xDyH3q+JSmtsMeSleJffarEBfG2yAFeJiFA4BnGOK6FFPHc19g=="
|
||||||
|
},
|
||||||
"@sindresorhus/is": {
|
"@sindresorhus/is": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz",
|
||||||
@@ -64,6 +185,11 @@
|
|||||||
"defer-to-connect": "^2.0.0"
|
"defer-to-connect": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@tokenizer/token": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
|
||||||
|
},
|
||||||
"@types/cacheable-request": {
|
"@types/cacheable-request": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||||
@@ -109,11 +235,29 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/uuid": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="
|
||||||
|
},
|
||||||
|
"@types/ws": {
|
||||||
|
"version": "8.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
|
||||||
|
"integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@ungap/promise-all-settled": {
|
"@ungap/promise-all-settled": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
|
||||||
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
|
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
|
||||||
},
|
},
|
||||||
|
"@vladfrangu/async_event_emitter": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-XtUEAS0m6uVddXW+EImGunLiJZzWNWAZQBoQCUneowrYXPQ6y7c0iWEm/wVYyGpTixTIhUfLRSoYCwojL64htA=="
|
||||||
|
},
|
||||||
"abstract-logging": {
|
"abstract-logging": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
||||||
@@ -255,7 +399,7 @@
|
|||||||
"array-flatten": {
|
"array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||||
},
|
},
|
||||||
"array.prototype.findindex": {
|
"array.prototype.findindex": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
@@ -287,11 +431,11 @@
|
|||||||
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
|
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
|
||||||
},
|
},
|
||||||
"async-mutex": {
|
"async-mutex": {
|
||||||
"version": "0.3.1",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz",
|
||||||
"integrity": "sha512-vRfQwcqBnJTLzVQo72Sf7KIUbcSUP5hNchx6udI1U6LuPQpfePgdjJzlCe76yFZ8pxlLjn9lwcl/Ya0TSOv0Tw==",
|
"integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"asynckit": {
|
"asynckit": {
|
||||||
@@ -393,20 +537,22 @@
|
|||||||
"integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
|
"integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
|
||||||
},
|
},
|
||||||
"body-parser": {
|
"body-parser": {
|
||||||
"version": "1.19.2",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||||
"integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==",
|
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "~1.1.2",
|
"depd": "2.0.0",
|
||||||
"http-errors": "1.8.1",
|
"destroy": "1.2.0",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "0.4.24",
|
||||||
"on-finished": "~2.3.0",
|
"on-finished": "2.4.1",
|
||||||
"qs": "6.9.7",
|
"qs": "6.11.0",
|
||||||
"raw-body": "2.4.3",
|
"raw-body": "2.5.1",
|
||||||
"type-is": "~1.6.18"
|
"type-is": "~1.6.18",
|
||||||
|
"unpipe": "1.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": {
|
"bytes": {
|
||||||
@@ -623,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",
|
||||||
@@ -717,9 +868,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"content-type": {
|
"content-type": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
|
||||||
},
|
},
|
||||||
"cookie": {
|
"cookie": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
@@ -828,20 +979,25 @@
|
|||||||
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
|
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
|
||||||
},
|
},
|
||||||
"depd": {
|
"depd": {
|
||||||
"version": "1.1.2",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
|
||||||
},
|
},
|
||||||
"destroy": {
|
"destroy": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
|
||||||
},
|
},
|
||||||
"diff": {
|
"diff": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
|
||||||
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w=="
|
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w=="
|
||||||
},
|
},
|
||||||
|
"discord-api-types": {
|
||||||
|
"version": "0.37.40",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.40.tgz",
|
||||||
|
"integrity": "sha512-LMALvtO+p6ERK8rwWoaI490NfIE/egbqjR4/rfLL1z9gQE1gqLiTpIUUDIunfAtKYzeH6ucyXhaXXWpfZh/Q6g=="
|
||||||
|
},
|
||||||
"duplexer2": {
|
"duplexer2": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||||
@@ -891,7 +1047,7 @@
|
|||||||
"ee-first": {
|
"ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||||
},
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -901,7 +1057,7 @@
|
|||||||
"encodeurl": {
|
"encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
||||||
},
|
},
|
||||||
"end-of-stream": {
|
"end-of-stream": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
@@ -1012,7 +1168,7 @@
|
|||||||
"escape-html": {
|
"escape-html": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
||||||
},
|
},
|
||||||
"escape-string-regexp": {
|
"escape-string-regexp": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -1027,7 +1183,7 @@
|
|||||||
"etag": {
|
"etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
|
||||||
},
|
},
|
||||||
"eventemitter3": {
|
"eventemitter3": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
@@ -1052,37 +1208,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"express": {
|
"express": {
|
||||||
"version": "4.17.3",
|
"version": "4.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||||
"integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==",
|
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.19.2",
|
"body-parser": "1.20.1",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.4.2",
|
"cookie": "0.5.0",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "~1.1.2",
|
"depd": "2.0.0",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~1.0.2",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"etag": "~1.8.1",
|
"etag": "~1.8.1",
|
||||||
"finalhandler": "~1.1.2",
|
"finalhandler": "1.2.0",
|
||||||
"fresh": "0.5.2",
|
"fresh": "0.5.2",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
"merge-descriptors": "1.0.1",
|
"merge-descriptors": "1.0.1",
|
||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "~2.3.0",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.7",
|
"path-to-regexp": "0.1.7",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.9.7",
|
"qs": "6.11.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
"send": "0.17.2",
|
"send": "0.18.0",
|
||||||
"serve-static": "1.14.2",
|
"serve-static": "1.15.0",
|
||||||
"setprototypeof": "1.2.0",
|
"setprototypeof": "1.2.0",
|
||||||
"statuses": "~1.5.0",
|
"statuses": "2.0.1",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
"utils-merge": "1.0.1",
|
"utils-merge": "1.0.1",
|
||||||
"vary": "~1.1.2"
|
"vary": "~1.1.2"
|
||||||
@@ -1097,6 +1254,11 @@
|
|||||||
"negotiator": "0.6.3"
|
"negotiator": "0.6.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cookie": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||||
|
},
|
||||||
"mime-db": {
|
"mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
@@ -1122,6 +1284,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"express-session": {
|
||||||
|
"version": "1.17.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
|
||||||
|
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
|
||||||
|
"requires": {
|
||||||
|
"cookie": "0.4.2",
|
||||||
|
"cookie-signature": "1.0.6",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"on-headers": "~1.0.2",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"uid-safe": "~2.1.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
|
||||||
|
},
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"extend": {
|
"extend": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
@@ -1169,16 +1358,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"finalhandler": {
|
"finalhandler": {
|
||||||
"version": "1.1.2",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||||
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
|
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~1.0.2",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"on-finished": "~2.3.0",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"statuses": "~1.5.0",
|
"statuses": "2.0.1",
|
||||||
"unpipe": "~1.0.0"
|
"unpipe": "~1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1256,7 +1445,7 @@
|
|||||||
"fresh": {
|
"fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
|
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
|
||||||
},
|
},
|
||||||
"fs-constants": {
|
"fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -1521,19 +1710,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"http-cache-semantics": {
|
"http-cache-semantics": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||||
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
|
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
|
||||||
},
|
},
|
||||||
"http-errors": {
|
"http-errors": {
|
||||||
"version": "1.8.1",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
|
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"depd": "~1.1.2",
|
"depd": "2.0.0",
|
||||||
"inherits": "2.0.4",
|
"inherits": "2.0.4",
|
||||||
"setprototypeof": "1.2.0",
|
"setprototypeof": "1.2.0",
|
||||||
"statuses": ">= 1.5.0 < 2",
|
"statuses": "2.0.1",
|
||||||
"toidentifier": "1.0.1"
|
"toidentifier": "1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1852,26 +2041,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jsonwebtoken": {
|
"jsonwebtoken": {
|
||||||
"version": "8.5.1",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
|
||||||
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
|
"integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"jws": "^3.2.2",
|
"jws": "^3.2.2",
|
||||||
"lodash.includes": "^4.3.0",
|
"lodash": "^4.17.21",
|
||||||
"lodash.isboolean": "^3.0.3",
|
|
||||||
"lodash.isinteger": "^4.0.4",
|
|
||||||
"lodash.isnumber": "^3.0.3",
|
|
||||||
"lodash.isplainobject": "^4.0.6",
|
|
||||||
"lodash.isstring": "^4.0.1",
|
|
||||||
"lodash.once": "^4.0.0",
|
|
||||||
"ms": "^2.1.1",
|
"ms": "^2.1.1",
|
||||||
"semver": "^5.6.0"
|
"semver": "^7.3.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2016,41 +2199,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||||
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
|
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
|
||||||
},
|
},
|
||||||
"lodash.includes": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
|
||||||
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
|
|
||||||
},
|
|
||||||
"lodash.isboolean": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
|
||||||
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
|
||||||
},
|
|
||||||
"lodash.isinteger": {
|
|
||||||
"version": "4.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
|
||||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
|
||||||
},
|
|
||||||
"lodash.isnumber": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
|
||||||
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
|
|
||||||
},
|
|
||||||
"lodash.isplainobject": {
|
"lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
|
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
|
||||||
},
|
},
|
||||||
"lodash.isstring": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
|
||||||
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
|
|
||||||
},
|
|
||||||
"lodash.once": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
|
||||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
|
||||||
},
|
|
||||||
"lodash.union": {
|
"lodash.union": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||||
@@ -2139,9 +2292,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"luxon": {
|
"luxon": {
|
||||||
"version": "1.28.0",
|
"version": "1.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz",
|
||||||
"integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ=="
|
"integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw=="
|
||||||
},
|
},
|
||||||
"md5": {
|
"md5": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
@@ -2167,7 +2320,7 @@
|
|||||||
"merge-descriptors": {
|
"merge-descriptors": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||||
},
|
},
|
||||||
"merge-stream": {
|
"merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -2177,7 +2330,7 @@
|
|||||||
"methods": {
|
"methods": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
|
||||||
},
|
},
|
||||||
"mime": {
|
"mime": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
@@ -2449,11 +2602,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node-id3": {
|
"node-id3": {
|
||||||
"version": "0.1.16",
|
"version": "0.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.6.tgz",
|
||||||
"integrity": "sha512-neWBJZxwrWnnebqy0b6gOGpnOPu1l1ASlusVCJUlrgr55ksftcz3lPbP/h4KaFXN+WQX7hh+kmNwkj5DMAa7KA==",
|
"integrity": "sha512-w8GuKXLlPpDjTxLowCt/uYMhRQzED3cg2GdSG1i6RSGKeDzPvxlXeLQuQInKljahPZ0aDnmyX7FX8BbJOM7REg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"iconv-lite": "^0.4.15"
|
"iconv-lite": "0.6.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
|
||||||
|
"requires": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node-schedule": {
|
"node-schedule": {
|
||||||
@@ -2579,9 +2742,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"on-finished": {
|
"on-finished": {
|
||||||
"version": "2.3.0",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"ee-first": "1.1.1"
|
"ee-first": "1.1.1"
|
||||||
}
|
}
|
||||||
@@ -2678,12 +2841,13 @@
|
|||||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
||||||
},
|
},
|
||||||
"passport": {
|
"passport": {
|
||||||
"version": "0.4.1",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
|
||||||
"integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==",
|
"integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"passport-strategy": "1.x.x",
|
"passport-strategy": "1.x.x",
|
||||||
"pause": "0.0.1"
|
"pause": "0.0.1",
|
||||||
|
"utils-merge": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"passport-http": {
|
"passport-http": {
|
||||||
@@ -2769,12 +2933,17 @@
|
|||||||
"path-to-regexp": {
|
"path-to-regexp": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||||
},
|
},
|
||||||
"pause": {
|
"pause": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||||
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
|
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||||
|
},
|
||||||
|
"peek-readable": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A=="
|
||||||
},
|
},
|
||||||
"performance-now": {
|
"performance-now": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
@@ -2843,9 +3012,17 @@
|
|||||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||||
},
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.9.7",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||||
"integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw=="
|
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||||
|
"requires": {
|
||||||
|
"side-channel": "^1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"random-bytes": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="
|
||||||
},
|
},
|
||||||
"randombytes": {
|
"randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
@@ -2861,12 +3038,12 @@
|
|||||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||||
},
|
},
|
||||||
"raw-body": {
|
"raw-body": {
|
||||||
"version": "2.4.3",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||||
"integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==",
|
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
"http-errors": "1.8.1",
|
"http-errors": "2.0.0",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "0.4.24",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "1.0.0"
|
||||||
},
|
},
|
||||||
@@ -2896,6 +3073,14 @@
|
|||||||
"util-deprecate": "^1.0.1"
|
"util-deprecate": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"readable-web-to-node-stream": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
|
||||||
|
"requires": {
|
||||||
|
"readable-stream": "^3.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"readdir-glob": {
|
"readdir-glob": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz",
|
||||||
@@ -2905,13 +3090,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"regexp.prototype.flags": {
|
"regexp.prototype.flags": {
|
||||||
"version": "1.4.3",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz",
|
||||||
"integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
|
"integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"call-bind": "^1.0.2",
|
"call-bind": "^1.0.2",
|
||||||
"define-properties": "^1.1.3",
|
"define-properties": "^1.2.0",
|
||||||
"functions-have-names": "^1.2.2"
|
"functions-have-names": "^1.2.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"define-properties": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
|
||||||
|
"requires": {
|
||||||
|
"has-property-descriptors": "^1.0.0",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
@@ -3072,28 +3268,31 @@
|
|||||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.7.1",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz",
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
"integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==",
|
||||||
|
"requires": {
|
||||||
|
"lru-cache": "^6.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"send": {
|
"send": {
|
||||||
"version": "0.17.2",
|
"version": "0.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||||
"integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
|
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "~1.1.2",
|
"depd": "2.0.0",
|
||||||
"destroy": "~1.0.4",
|
"destroy": "1.2.0",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~1.0.2",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"etag": "~1.8.1",
|
"etag": "~1.8.1",
|
||||||
"fresh": "0.5.2",
|
"fresh": "0.5.2",
|
||||||
"http-errors": "1.8.1",
|
"http-errors": "2.0.0",
|
||||||
"mime": "1.6.0",
|
"mime": "1.6.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"on-finished": "~2.3.0",
|
"on-finished": "2.4.1",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
"statuses": "~1.5.0"
|
"statuses": "2.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": {
|
"ms": {
|
||||||
@@ -3112,14 +3311,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve-static": {
|
"serve-static": {
|
||||||
"version": "1.14.2",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||||
"integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
|
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~1.0.2",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"send": "0.17.2"
|
"send": "0.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setimmediate": {
|
"setimmediate": {
|
||||||
@@ -3217,9 +3416,9 @@
|
|||||||
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
|
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
|
||||||
},
|
},
|
||||||
"statuses": {
|
"statuses": {
|
||||||
"version": "1.5.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
|
||||||
},
|
},
|
||||||
"stealthy-require": {
|
"stealthy-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -3325,6 +3524,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
||||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
|
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
|
||||||
},
|
},
|
||||||
|
"strtok3": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==",
|
||||||
|
"requires": {
|
||||||
|
"@tokenizer/token": "^0.3.0",
|
||||||
|
"peek-readable": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"table-parser": {
|
"table-parser": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/table-parser/-/table-parser-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/table-parser/-/table-parser-0.1.3.tgz",
|
||||||
@@ -3384,6 +3592,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
|
||||||
},
|
},
|
||||||
|
"token-types": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==",
|
||||||
|
"requires": {
|
||||||
|
"@tokenizer/token": "^0.3.0",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tough-cookie": {
|
"tough-cookie": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||||
@@ -3408,10 +3625,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
|
||||||
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
|
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
|
||||||
},
|
},
|
||||||
|
"ts-mixer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ=="
|
||||||
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.3.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
|
||||||
},
|
},
|
||||||
"tunnel-agent": {
|
"tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
@@ -3455,6 +3677,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||||
},
|
},
|
||||||
|
"uid-safe": {
|
||||||
|
"version": "2.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||||
|
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||||
|
"requires": {
|
||||||
|
"random-bytes": "~1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"unbox-primitive": {
|
"unbox-primitive": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
||||||
@@ -3466,6 +3696,14 @@
|
|||||||
"which-boxed-primitive": "^1.0.2"
|
"which-boxed-primitive": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"undici": {
|
||||||
|
"version": "5.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz",
|
||||||
|
"integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==",
|
||||||
|
"requires": {
|
||||||
|
"busboy": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"universalify": {
|
"universalify": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
|
||||||
@@ -3474,7 +3712,7 @@
|
|||||||
"unpipe": {
|
"unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
|
||||||
},
|
},
|
||||||
"unzipper": {
|
"unzipper": {
|
||||||
"version": "0.10.10",
|
"version": "0.10.10",
|
||||||
@@ -3528,16 +3766,17 @@
|
|||||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||||
},
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"version": "7.0.2",
|
"version": "8.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
"integrity": "sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw=="
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||||
},
|
},
|
||||||
"uuidv4": {
|
"uuidv4": {
|
||||||
"version": "6.0.6",
|
"version": "6.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz",
|
||||||
"integrity": "sha512-10YcruyGJtsG5SJnPG+8atr8toJa7xAOrcO7B7plYYiwpH1mQ8UZHjNSa2MrwGi6KWuyVrXGHr+Rce22F9UAiw==",
|
"integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"uuid": "7.0.2"
|
"@types/uuid": "8.3.4",
|
||||||
|
"uuid": "8.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vary": {
|
"vary": {
|
||||||
@@ -3667,6 +3906,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||||
},
|
},
|
||||||
|
"ws": {
|
||||||
|
"version": "8.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||||
|
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA=="
|
||||||
|
},
|
||||||
"xml-js": {
|
"xml-js": {
|
||||||
"version": "1.6.11",
|
"version": "1.6.11",
|
||||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||||
|
|||||||
@@ -17,21 +17,29 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": ""
|
"url": ""
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16",
|
||||||
|
"npm": "6.14.4"
|
||||||
|
},
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@discordjs/builders": "^1.6.1",
|
||||||
|
"@discordjs/core": "^0.5.2",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"async": "^3.2.3",
|
"async": "^3.2.3",
|
||||||
"async-mutex": "^0.3.1",
|
"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.17.3",
|
"express": "^4.18.2",
|
||||||
|
"express-session": "^1.17.3",
|
||||||
"feed": "^4.2.2",
|
"feed": "^4.2.2",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^9.0.0",
|
||||||
"gotify": "^1.1.0",
|
"gotify": "^1.1.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lowdb": "^1.0.0",
|
"lowdb": "^1.0.0",
|
||||||
"md5": "^2.2.1",
|
"md5": "^2.2.1",
|
||||||
@@ -40,10 +48,10 @@
|
|||||||
"mongodb": "^3.6.9",
|
"mongodb": "^3.6.9",
|
||||||
"multer": "1.4.5-lts.1",
|
"multer": "1.4.5-lts.1",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"node-id3": "^0.1.14",
|
"node-id3": "^0.2.6",
|
||||||
"node-schedule": "^2.1.0",
|
"node-schedule": "^2.1.0",
|
||||||
"node-telegram-bot-api": "^0.61.0",
|
"node-telegram-bot-api": "^0.61.0",
|
||||||
"passport": "^0.4.1",
|
"passport": "^0.6.0",
|
||||||
"passport-http": "^0.3.0",
|
"passport-http": "^0.3.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-ldapauth": "^3.0.1",
|
"passport-ldapauth": "^3.0.1",
|
||||||
@@ -54,7 +62,7 @@
|
|||||||
"rxjs": "^7.3.0",
|
"rxjs": "^7.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"unzipper": "^0.10.10",
|
"unzipper": "^0.10.10",
|
||||||
"uuidv4": "^6.0.6",
|
"uuidv4": "^6.2.13",
|
||||||
"winston": "^3.7.2",
|
"winston": "^3.7.2",
|
||||||
"xmlbuilder2": "^3.0.2",
|
"xmlbuilder2": "^3.0.2",
|
||||||
"youtube-dl": "^3.0.2"
|
"youtube-dl": "^3.0.2"
|
||||||
|
|||||||
@@ -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.')
|
||||||
}
|
}
|
||||||
@@ -92,7 +94,10 @@ async function getSubscriptionInfo(sub) {
|
|||||||
}
|
}
|
||||||
// if it's now valid, update
|
// if it's now valid, update
|
||||||
if (sub.name) {
|
if (sub.name) {
|
||||||
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub.name});
|
let sub_name = sub.name;
|
||||||
|
const sub_name_exists = await db_api.getRecord('subscriptions', {name: sub.name, isPlaylist: sub.isPlaylist, user_uid: sub.user_uid});
|
||||||
|
if (sub_name_exists) sub_name += ` - ${sub.id}`;
|
||||||
|
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub_name});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,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');
|
||||||
@@ -145,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});
|
||||||
@@ -196,8 +201,13 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
|||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// check if the user wants the video to be redownloaded (deleteForever === false)
|
// check if the user wants the video to be redownloaded (deleteForever === false)
|
||||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
if (deleteForever) {
|
||||||
if (useArchive && !deleteForever) {
|
// ensure video is in the archives
|
||||||
|
const exists_in_archive = await archive_api.existsInArchive(retrievedExtractor, retrievedID, sub.type, user_uid, sub.id);
|
||||||
|
if (!exists_in_archive) {
|
||||||
|
await archive_api.addToArchive(retrievedExtractor, retrievedID, sub.type, file.title, user_uid, sub.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
await archive_api.removeFromArchive(retrievedExtractor, retrievedID, sub.type, user_uid, sub.id);
|
await archive_api.removeFromArchive(retrievedExtractor, retrievedID, sub.type, user_uid, sub.id);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -208,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;
|
||||||
}
|
}
|
||||||
@@ -229,13 +239,20 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
|
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
|
||||||
|
|
||||||
// get videos
|
// get videos
|
||||||
logger.verbose(`Subscription: getting videos for subscription ${sub.name} with args: ${downloadConfig.join(',')}`);
|
logger.verbose(`Subscription: getting list of videos to download for ${sub.name} with args: ${downloadConfig.join(',')}`);
|
||||||
|
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
|
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
|
||||||
// cleanup
|
// cleanup
|
||||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
||||||
|
|
||||||
|
// remove temporary archive file if it exists
|
||||||
|
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||||
|
const archive_exists = await fs.pathExists(archive_path);
|
||||||
|
if (archive_exists) {
|
||||||
|
await fs.unlink(archive_path);
|
||||||
|
}
|
||||||
|
|
||||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||||
if (err && !output) {
|
if (err && !output) {
|
||||||
logger.error(err.stderr ? err.stderr : err.message);
|
logger.error(err.stderr ? err.stderr : err.message);
|
||||||
@@ -290,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];
|
||||||
@@ -301,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');
|
||||||
@@ -354,6 +371,13 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
|
|
||||||
downloadConfig.push(...qualityPath)
|
downloadConfig.push(...qualityPath)
|
||||||
|
|
||||||
|
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
||||||
|
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
||||||
|
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
|
||||||
|
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||||
|
await fs.writeFile(archive_path, archive_text);
|
||||||
|
downloadConfig.push('--download-archive', archive_path);
|
||||||
|
|
||||||
if (sub.custom_args) {
|
if (sub.custom_args) {
|
||||||
const customArgsArray = sub.custom_args.split(',,');
|
const customArgsArray = sub.custom_args.split(',,');
|
||||||
if (customArgsArray.indexOf('-f') !== -1) {
|
if (customArgsArray.indexOf('-f') !== -1) {
|
||||||
@@ -408,11 +432,8 @@ async function getFilesToDownload(sub, output_jsons) {
|
|||||||
logger.info(`Skipping adding file ${output_json['_filename']} for subscription ${sub.name} as a file with that path already exists.`)
|
logger.info(`Skipping adding file ${output_json['_filename']} for subscription ${sub.name} as a file with that path already exists.`)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
const exists_in_archive = await archive_api.existsInArchive(output_json['extractor'], output_json['id'], sub.type, sub.user_uid, sub.id);
|
||||||
if (useYoutubeDLArchive) {
|
if (exists_in_archive) continue;
|
||||||
const exists_in_archive = await archive_api.existsInArchive(output_json['extractor'], output_json['id'], sub.type, sub.user_uid, sub.id);
|
|
||||||
if (exists_in_archive) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
files_to_download.push(output_json);
|
files_to_download.push(output_json);
|
||||||
}
|
}
|
||||||
@@ -421,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);
|
||||||
});
|
});
|
||||||
@@ -461,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;
|
||||||
@@ -515,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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ const db_api = require('./db');
|
|||||||
const notifications_api = require('./notifications');
|
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 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: {
|
||||||
@@ -20,7 +28,7 @@ const TASKS = {
|
|||||||
job: null
|
job: null
|
||||||
},
|
},
|
||||||
missing_db_records: {
|
missing_db_records: {
|
||||||
run: db_api.importUnregisteredFiles,
|
run: files_api.importUnregisteredFiles,
|
||||||
title: 'Import missing DB records',
|
title: 'Import missing DB records',
|
||||||
job: null
|
job: null
|
||||||
},
|
},
|
||||||
@@ -46,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +114,7 @@ exports.setupTasks = async () => {
|
|||||||
const tasks_keys = Object.keys(TASKS);
|
const tasks_keys = Object.keys(TASKS);
|
||||||
for (let i = 0; i < tasks_keys.length; i++) {
|
for (let i = 0; i < tasks_keys.length; i++) {
|
||||||
const task_key = tasks_keys[i];
|
const task_key = tasks_keys[i];
|
||||||
const mergedDefaultOptions = Object.assign(defaultOptions['all'], defaultOptions[task_key] || {});
|
const mergedDefaultOptions = Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {});
|
||||||
const task_in_db = await db_api.getRecord('tasks', {key: task_key});
|
const task_in_db = await db_api.getRecord('tasks', {key: task_key});
|
||||||
if (!task_in_db) {
|
if (!task_in_db) {
|
||||||
// insert task metadata into table if missing, eventually move title to UI
|
// insert task metadata into table if missing, eventually move title to UI
|
||||||
@@ -115,14 +128,16 @@ exports.setupTasks = async () => {
|
|||||||
data: null,
|
data: null,
|
||||||
error: null,
|
error: null,
|
||||||
schedule: null,
|
schedule: null,
|
||||||
options: Object.assign(defaultOptions['all'], defaultOptions[task_key] || {})
|
options: Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// verify all options exist in task
|
// verify all options exist in task
|
||||||
for (const key of Object.keys(mergedDefaultOptions)) {
|
for (const key of Object.keys(mergedDefaultOptions)) {
|
||||||
|
const option_key = `options.${key}`;
|
||||||
|
// Remove any potential mangled option keys (#861)
|
||||||
|
await db_api.removePropertyFromRecord('tasks', {key: task_key}, {[option_key]: true});
|
||||||
if (!(task_in_db.options && task_in_db.options.hasOwnProperty(key))) {
|
if (!(task_in_db.options && task_in_db.options.hasOwnProperty(key))) {
|
||||||
const option_key = `options.${key}`
|
await db_api.updateRecord('tasks', {key: task_key}, {[option_key]: mergedDefaultOptions[key]}, true);
|
||||||
await db_api.updateRecord('tasks', {key: task_key}, {[option_key]: mergedDefaultOptions[key]});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,9 +272,73 @@ async function autoDeleteFiles(data) {
|
|||||||
logger.info(`Removing ${data['files_to_remove'].length} old files!`);
|
logger.info(`Removing ${data['files_to_remove'].length} old files!`);
|
||||||
for (let i = 0; i < data['files_to_remove'].length; i++) {
|
for (let i = 0; i < data['files_to_remove'].length; i++) {
|
||||||
const file_to_remove = data['files_to_remove'][i];
|
const file_to_remove = data['files_to_remove'][i];
|
||||||
await db_api.deleteFile(file_to_remove['uid'], task_obj['options']['blacklist_files'] || (file_to_remove['sub_id'] && file_to_remove['blacklist_subscription_files']));
|
await files_api.deleteFile(file_to_remove['uid'], task_obj['options']['blacklist_files'] || (file_to_remove['sub_id'] && file_to_remove['blacklist_subscription_files']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
@@ -38,6 +39,8 @@ var db_api = require('../db');
|
|||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
const subscriptions_api = require('../subscriptions');
|
const subscriptions_api = require('../subscriptions');
|
||||||
const archive_api = require('../archive');
|
const archive_api = require('../archive');
|
||||||
|
const categories_api = require('../categories');
|
||||||
|
const files_api = require('../files');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
const NodeID3 = require('node-id3');
|
const NodeID3 = require('node-id3');
|
||||||
@@ -175,6 +178,24 @@ describe('Database', async function() {
|
|||||||
await db_api.removeRecord('test', {test_update: 'test'});
|
await db_api.removeRecord('test', {test_update: 'test'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Update records', async function() {
|
||||||
|
await db_api.insertRecordIntoTable('test', {test_update: 'test', key: 'test1'});
|
||||||
|
await db_api.insertRecordIntoTable('test', {test_update: 'test', key: 'test2'});
|
||||||
|
await db_api.updateRecords('test', {test_update: 'test'}, {added_field: true});
|
||||||
|
const updated_records = await db_api.getRecords('test', {added_field: true});
|
||||||
|
assert(updated_records.length === 2);
|
||||||
|
await db_api.removeRecord('test', {test_update: 'test'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Remove property from record', async function() {
|
||||||
|
await db_api.insertRecordIntoTable('test', {test_keep: 'test', test_remove: 'test'});
|
||||||
|
await db_api.removePropertyFromRecord('test', {test_keep: 'test'}, {test_remove: true});
|
||||||
|
const updated_record = await db_api.getRecord('test', {test_keep: 'test'});
|
||||||
|
assert(updated_record['test_keep']);
|
||||||
|
assert(!updated_record['test_remove']);
|
||||||
|
await db_api.removeRecord('test', {test_keep: 'test'});
|
||||||
|
});
|
||||||
|
|
||||||
it('Remove record', async function() {
|
it('Remove record', async function() {
|
||||||
await db_api.insertRecordIntoTable('test', {test_remove: 'test'});
|
await db_api.insertRecordIntoTable('test', {test_remove: 'test'});
|
||||||
const delete_succeeded = await db_api.removeRecord('test', {test_remove: 'test'});
|
const delete_succeeded = await db_api.removeRecord('test', {test_remove: 'test'});
|
||||||
@@ -316,48 +337,56 @@ 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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Video player - normal', async function() {
|
describe('Video player - normal', async function() {
|
||||||
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
|
beforeEach(async function() {
|
||||||
await db_api.insertRecordIntoTable('files', sample_video_json);
|
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
|
||||||
|
await db_api.insertRecordIntoTable('files', sample_video_json);
|
||||||
|
});
|
||||||
const video_to_test = sample_video_json['uid'];
|
const video_to_test = sample_video_json['uid'];
|
||||||
it('Get video', async function() {
|
it('Get video', async function() {
|
||||||
const video_obj = await db_api.getVideo(video_to_test);
|
const video_obj = await files_api.getVideo(video_to_test);
|
||||||
assert(video_obj);
|
assert(video_obj);
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Zip generators', function() {
|
describe('Zip generators', function() {
|
||||||
it('Playlist zip generator', async function() {
|
it('Playlist zip generator', async function() {
|
||||||
const playlist = await db_api.getPlaylist(playlist_to_test, user_to_test);
|
const playlist = await files_api.getPlaylist(playlist_to_test, user_to_test);
|
||||||
assert(playlist);
|
assert(playlist);
|
||||||
const playlist_files_to_download = [];
|
const playlist_files_to_download = [];
|
||||||
for (let i = 0; i < playlist['uids'].length; i++) {
|
for (let i = 0; i < playlist['uids'].length; i++) {
|
||||||
const uid = playlist['uids'][i];
|
const uid = playlist['uids'][i];
|
||||||
const playlist_file = await db_api.getVideo(uid, user_to_test);
|
const playlist_file = await files_api.getVideo(uid, user_to_test);
|
||||||
playlist_files_to_download.push(playlist_file);
|
playlist_files_to_download.push(playlist_file);
|
||||||
}
|
}
|
||||||
const zip_path = await utils.createContainerZipFile(playlist, playlist_files_to_download);
|
const zip_path = await utils.createContainerZipFile(playlist, playlist_files_to_download);
|
||||||
@@ -385,7 +414,7 @@ describe('Multi User', async function() {
|
|||||||
// const sub_to_test = '';
|
// const sub_to_test = '';
|
||||||
// const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e';
|
// const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e';
|
||||||
// it('Get video', async function() {
|
// it('Get video', async function() {
|
||||||
// const video_obj = db_api.getVideo(video_to_test, 'admin', );
|
// const video_obj = files_api.getVideo(video_to_test, 'admin', );
|
||||||
// assert(video_obj);
|
// assert(video_obj);
|
||||||
// });
|
// });
|
||||||
|
|
||||||
@@ -488,18 +517,23 @@ describe('Downloader', function() {
|
|||||||
const new_args1 = ['--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
|
const new_args1 = ['--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
|
||||||
const updated_args1 = utils.injectArgs(original_args1, new_args1);
|
const updated_args1 = utils.injectArgs(original_args1, new_args1);
|
||||||
const expected_args1 = ['--no-resize-buffer', '--no-mtime', '--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
|
const expected_args1 = ['--no-resize-buffer', '--no-mtime', '--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
|
||||||
assert(JSON.stringify(updated_args1), JSON.stringify(expected_args1));
|
assert(JSON.stringify(updated_args1) === JSON.stringify(expected_args1));
|
||||||
|
|
||||||
const original_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3'];
|
const original_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3'];
|
||||||
const new_args2 = ['--add-metadata', '--embed-thumbnail', '--convert-thumbnails', 'jpg'];
|
const new_args2 = ['--add-metadata', '--embed-thumbnail', '--convert-thumbnails', 'jpg'];
|
||||||
const updated_args2 = utils.injectArgs(original_args2, new_args2);
|
const updated_args2 = utils.injectArgs(original_args2, new_args2);
|
||||||
const expected_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3', '--add-metadata', '--embed-thumbnail', '--convert_thumbnails', 'jpg'];
|
const expected_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3', '--add-metadata', '--embed-thumbnail', '--convert-thumbnails', 'jpg'];
|
||||||
console.log(updated_args2);
|
assert(JSON.stringify(updated_args2) === JSON.stringify(expected_args2));
|
||||||
assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2));
|
|
||||||
|
const original_args3 = ['-o', '%(title)s.%(ext)s'];
|
||||||
|
const new_args3 = ['--min-filesize','1'];
|
||||||
|
const updated_args3 = utils.injectArgs(original_args3, new_args3);
|
||||||
|
const expected_args3 = ['-o', '%(title)s.%(ext)s', '--min-filesize', '1'];
|
||||||
|
assert(JSON.stringify(updated_args3) === JSON.stringify(expected_args3));
|
||||||
});
|
});
|
||||||
describe('Twitch', async function () {
|
describe('Twitch', async function () {
|
||||||
const twitch_api = require('../twitch');
|
const twitch_api = require('../twitch');
|
||||||
const example_vod = '1493770675';
|
const example_vod = '1710641401';
|
||||||
it('Download VOD', async function() {
|
it('Download VOD', async function() {
|
||||||
const sample_path = path.join('test', 'sample.twitch_chat.json');
|
const sample_path = path.join('test', 'sample.twitch_chat.json');
|
||||||
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
||||||
@@ -581,7 +615,7 @@ describe('Tasks', function() {
|
|||||||
fs.copyFileSync('test/sample.mp4', 'video/sample.mp4');
|
fs.copyFileSync('test/sample.mp4', 'video/sample.mp4');
|
||||||
await tasks_api.executeTask('missing_db_records');
|
await tasks_api.executeTask('missing_db_records');
|
||||||
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
|
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
|
||||||
assert(!!imported_file, true);
|
assert(!!imported_file === true);
|
||||||
|
|
||||||
// post-test cleanup
|
// post-test cleanup
|
||||||
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
||||||
@@ -699,4 +733,129 @@ describe('Utils', async function() {
|
|||||||
const stripped_obj = utils.stripPropertiesFromObject(test_obj, ['test1', 'test3']);
|
const stripped_obj = utils.stripPropertiesFromObject(test_obj, ['test1', 'test3']);
|
||||||
assert(!stripped_obj['test1'] && stripped_obj['test2'] && !stripped_obj['test3'])
|
assert(!stripped_obj['test1'] && stripped_obj['test2'] && !stripped_obj['test3'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Convert flat object to nested object', async function() {
|
||||||
|
// No modfication
|
||||||
|
const flat_obj0 = {'test1': {'test_sub': true}, 'test2': {test_sub: true}};
|
||||||
|
const nested_obj0 = utils.convertFlatObjectToNestedObject(flat_obj0);
|
||||||
|
assert(nested_obj0['test1'] && nested_obj0['test1']['test_sub']);
|
||||||
|
assert(nested_obj0['test2'] && nested_obj0['test2']['test_sub']);
|
||||||
|
|
||||||
|
// Standard setup
|
||||||
|
const flat_obj1 = {'test1.test_sub': true, 'test2.test_sub': true};
|
||||||
|
const nested_obj1 = utils.convertFlatObjectToNestedObject(flat_obj1);
|
||||||
|
assert(nested_obj1['test1'] && nested_obj1['test1']['test_sub']);
|
||||||
|
assert(nested_obj1['test2'] && nested_obj1['test2']['test_sub']);
|
||||||
|
|
||||||
|
// Nested branches
|
||||||
|
const flat_obj2 = {'test1.test_sub': true, 'test1.test2.test_sub': true};
|
||||||
|
const nested_obj2 = utils.convertFlatObjectToNestedObject(flat_obj2);
|
||||||
|
assert(nested_obj2['test1'] && nested_obj2['test1']['test_sub']);
|
||||||
|
assert(nested_obj2['test1'] && nested_obj2['test1']['test2'] && nested_obj2['test1']['test2']['test_sub']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Categories', async function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
await db_api.connectToDB();
|
||||||
|
const new_category = {
|
||||||
|
name: 'test_category',
|
||||||
|
uid: uuid(),
|
||||||
|
rules: [],
|
||||||
|
custom_output: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
await db_api.insertRecordIntoTable('categories', new_category);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function() {
|
||||||
|
await db_api.removeAllRecords('categories', {name: 'test_category'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Categorize - includes', async function() {
|
||||||
|
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
||||||
|
preceding_operator: null,
|
||||||
|
comparator: 'includes',
|
||||||
|
property: 'title',
|
||||||
|
value: 'Sample'
|
||||||
|
});
|
||||||
|
|
||||||
|
const category = await categories_api.categorize([sample_video_json]);
|
||||||
|
assert(category && category.name === 'test_category');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Categorize - not includes', async function() {
|
||||||
|
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
||||||
|
preceding_operator: null,
|
||||||
|
comparator: 'not_includes',
|
||||||
|
property: 'title',
|
||||||
|
value: 'Sample'
|
||||||
|
});
|
||||||
|
|
||||||
|
const category = await categories_api.categorize([sample_video_json]);
|
||||||
|
assert(!category);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Categorize - equals', async function() {
|
||||||
|
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
||||||
|
preceding_operator: null,
|
||||||
|
comparator: 'equals',
|
||||||
|
property: 'uploader',
|
||||||
|
value: 'Sample Uploader'
|
||||||
|
});
|
||||||
|
|
||||||
|
const category = await categories_api.categorize([sample_video_json]);
|
||||||
|
console.log(category);
|
||||||
|
assert(category && category.name === 'test_category');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Categorize - not equals', async function() {
|
||||||
|
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
||||||
|
preceding_operator: null,
|
||||||
|
comparator: 'not_equals',
|
||||||
|
property: 'uploader',
|
||||||
|
value: 'Sample Uploader'
|
||||||
|
});
|
||||||
|
|
||||||
|
const category = await categories_api.categorize([sample_video_json]);
|
||||||
|
assert(!category);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Categorize - AND', async function() {
|
||||||
|
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
||||||
|
preceding_operator: null,
|
||||||
|
comparator: 'equals',
|
||||||
|
property: 'uploader',
|
||||||
|
value: 'Sample Uploader'
|
||||||
|
});
|
||||||
|
|
||||||
|
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
||||||
|
preceding_operator: 'and',
|
||||||
|
comparator: 'not_includes',
|
||||||
|
property: 'title',
|
||||||
|
value: 'Sample'
|
||||||
|
});
|
||||||
|
|
||||||
|
const category = await categories_api.categorize([sample_video_json]);
|
||||||
|
assert(!category);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Categorize - OR', async function() {
|
||||||
|
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
||||||
|
preceding_operator: null,
|
||||||
|
comparator: 'equals',
|
||||||
|
property: 'uploader',
|
||||||
|
value: 'Sample Uploader'
|
||||||
|
});
|
||||||
|
|
||||||
|
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
||||||
|
preceding_operator: 'or',
|
||||||
|
comparator: 'not_includes',
|
||||||
|
property: 'title',
|
||||||
|
value: 'Sample'
|
||||||
|
});
|
||||||
|
|
||||||
|
const category = await categories_api.categorize([sample_video_json]);
|
||||||
|
assert(category);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -4,19 +4,29 @@ const logger = require('./logger');
|
|||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
const child_process = require('child_process');
|
||||||
|
const commandExistsSync = require('command-exists').sync;
|
||||||
|
|
||||||
async function getCommentsForVOD(clientID, clientSecret, vodId) {
|
async function getCommentsForVOD(vodId) {
|
||||||
const { promisify } = require('util');
|
|
||||||
const child_process = require('child_process');
|
|
||||||
const exec = promisify(child_process.exec);
|
const exec = promisify(child_process.exec);
|
||||||
|
|
||||||
// Reject invalid params to prevent command injection attack
|
// Reject invalid params to prevent command injection attack
|
||||||
if (!clientID.match(/^[0-9a-z]+$/) || !clientSecret.match(/^[0-9a-z]+$/) || !vodId.match(/^[0-9a-z]+$/)) {
|
if (!vodId.match(/^[0-9a-z]+$/)) {
|
||||||
logger.error('Client ID, client secret, and VOD ID must be purely alphanumeric. Twitch chat download failed!');
|
logger.error('VOD ID must be purely alphanumeric. Twitch chat download failed!');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await exec(`tcd --video ${vodId} --client-id ${clientID} --client-secret ${clientSecret} --format json -o appdata`, {stdio:[0,1,2]});
|
const is_windows = process.platform === 'win32';
|
||||||
|
const cliExt = is_windows ? '.exe' : ''
|
||||||
|
const cliPath = `TwitchDownloaderCLI${cliExt}`
|
||||||
|
|
||||||
|
if (!commandExistsSync(cliPath)) {
|
||||||
|
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await exec(`${cliPath} chatdownload -u ${vodId} -o appdata/${vodId}.json`, {stdio:[0,1,2]});
|
||||||
|
|
||||||
if (result['stderr']) {
|
if (result['stderr']) {
|
||||||
logger.error(`Failed to download twitch comments for ${vodId}`);
|
logger.error(`Failed to download twitch comments for ${vodId}`);
|
||||||
@@ -73,9 +83,7 @@ async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
|
|||||||
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
|
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
|
||||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
|
const chat = await getCommentsForVOD(vodId);
|
||||||
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
|
|
||||||
const chat = await getCommentsForVOD(twitch_client_id, twitch_client_secret, vodId);
|
|
||||||
|
|
||||||
// save file if needed params are included
|
// save file if needed params are included
|
||||||
let file_path = null;
|
let file_path = null;
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ exports.createZipFile = async (zip_file_path, file_paths) => {
|
|||||||
await archive.finalize();
|
await archive.finalize();
|
||||||
|
|
||||||
// wait a tiny bit for the zip to reload in fs
|
// wait a tiny bit for the zip to reload in fs
|
||||||
await wait(100);
|
await exports.wait(100);
|
||||||
return zip_file_path;
|
return zip_file_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,10 +414,11 @@ exports.injectArgs = (original_args, new_args) => {
|
|||||||
if (CONSTS.YTDL_ARGS_WITH_VALUES.has(new_arg)) {
|
if (CONSTS.YTDL_ARGS_WITH_VALUES.has(new_arg)) {
|
||||||
if (original_args.includes(new_arg)) {
|
if (original_args.includes(new_arg)) {
|
||||||
const original_index = original_args.indexOf(new_arg);
|
const original_index = original_args.indexOf(new_arg);
|
||||||
original_args.splice(original_index, 2);
|
updated_args.splice(original_index, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
updated_args.push(new_arg, new_args[i + 1]);
|
updated_args.push(new_arg, new_args[i + 1]);
|
||||||
|
i++; // we need to skip the arg value on the next loop
|
||||||
} else {
|
} else {
|
||||||
if (!original_args.includes(new_arg)) {
|
if (!original_args.includes(new_arg)) {
|
||||||
updated_args.push(new_arg);
|
updated_args.push(new_arg);
|
||||||
@@ -501,6 +502,34 @@ exports.updateLoggerLevel = (new_logger_level) => {
|
|||||||
logger.transports[2].level = new_logger_level;
|
logger.transports[2].level = new_logger_level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.convertFlatObjectToNestedObject = (obj) => {
|
||||||
|
const result = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
const nestedKeys = key.split('.');
|
||||||
|
let currentObj = result;
|
||||||
|
for (let i = 0; i < nestedKeys.length; i++) {
|
||||||
|
if (i === nestedKeys.length - 1) {
|
||||||
|
currentObj[nestedKeys[i]] = obj[key];
|
||||||
|
} else {
|
||||||
|
currentObj[nestedKeys[i]] = currentObj[nestedKeys[i]] || {};
|
||||||
|
currentObj = currentObj[nestedKeys[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ services:
|
|||||||
- "8998:17442"
|
- "8998:17442"
|
||||||
image: tzahi12345/youtubedl-material:latest
|
image: tzahi12345/youtubedl-material:latest
|
||||||
ytdl-mongo-db:
|
ytdl-mongo-db:
|
||||||
|
# If you are using a Raspberry Pi, use mongo:4.4.18
|
||||||
image: mongo:4
|
image: mongo:4
|
||||||
logging:
|
logging:
|
||||||
driver: "none"
|
driver: "none"
|
||||||
|
|||||||
69
docker-utils/GetTwitchDownloader.py
Normal file
69
docker-utils/GetTwitchDownloader.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import platform
|
||||||
|
import requests
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from github import Github
|
||||||
|
|
||||||
|
machine = platform.machine()
|
||||||
|
|
||||||
|
# https://stackoverflow.com/questions/45125516/possible-values-for-uname-m
|
||||||
|
MACHINES_TO_ZIP = OrderedDict([
|
||||||
|
("x86_64", "Linux-x64"),
|
||||||
|
("aarch64", "LinuxArm64"),
|
||||||
|
("armv8", "LinuxArm64"),
|
||||||
|
("arm", "LinuxArm"),
|
||||||
|
("AMD64", "Windows-x64")
|
||||||
|
])
|
||||||
|
|
||||||
|
def getZipName():
|
||||||
|
for possibleMachine, possibleZipName in MACHINES_TO_ZIP.items():
|
||||||
|
if possibleMachine in machine:
|
||||||
|
return possibleZipName
|
||||||
|
|
||||||
|
def getLatestFileInRepo(repo, search_string):
|
||||||
|
# Create an unauthenticated instance of the Github object
|
||||||
|
g = Github(os.environ.get('GH_TOKEN'))
|
||||||
|
|
||||||
|
# Replace with the repository owner and name
|
||||||
|
repo = g.get_repo(repo)
|
||||||
|
|
||||||
|
# Get all releases of the repository
|
||||||
|
releases = repo.get_releases()
|
||||||
|
|
||||||
|
# Loop through the releases in reverse order (from latest to oldest)
|
||||||
|
for release in list(releases):
|
||||||
|
# Get the release assets (files attached to the release)
|
||||||
|
assets = release.get_assets()
|
||||||
|
|
||||||
|
# Loop through the assets
|
||||||
|
for asset in assets:
|
||||||
|
if re.search(search_string, asset.name):
|
||||||
|
print(f'Downloading: {asset.name}')
|
||||||
|
response = requests.get(asset.browser_download_url)
|
||||||
|
with open(asset.name, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
print(f'Download complete: {asset.name}. Unzipping...')
|
||||||
|
shutil.unpack_archive(asset.name, './')
|
||||||
|
print(f'Unzipping complete!')
|
||||||
|
os.remove(asset.name)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# If no matching release is found, print a message
|
||||||
|
print(f'No release found with {search_string}')
|
||||||
|
|
||||||
|
def getLatestCLIRelease():
|
||||||
|
zipName = getZipName()
|
||||||
|
if not zipName:
|
||||||
|
print(f"GetTwitchDownloader.py could not get valid path for '{machine}'. Exiting...")
|
||||||
|
sys.exit(1)
|
||||||
|
searchString = r'.*CLI.*' + zipName
|
||||||
|
getLatestFileInRepo("lay295/TwitchDownloader", searchString)
|
||||||
|
|
||||||
|
getLatestCLIRelease()
|
||||||
39
docker-utils/fetch-twitchdownloader.sh
Normal file
39
docker-utils/fetch-twitchdownloader.sh
Normal 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
|
||||||
@@ -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"
|
||||||
1337
package-lock.json
generated
1337
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -34,20 +34,19 @@
|
|||||||
"@angular/platform-browser-dynamic": "^15.0.1",
|
"@angular/platform-browser-dynamic": "^15.0.1",
|
||||||
"@angular/router": "^15.0.1",
|
"@angular/router": "^15.0.1",
|
||||||
"@fontsource/material-icons": "^4.5.4",
|
"@fontsource/material-icons": "^4.5.4",
|
||||||
"@ngneat/content-loader": "^5.0.0",
|
"@ngneat/content-loader": "^7.0.0",
|
||||||
"@videogular/ngx-videogular": "^6.0.0",
|
"@videogular/ngx-videogular": "^6.0.0",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^10.0.7",
|
||||||
"fingerprintjs2": "^2.1.0",
|
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"material-icons": "^1.10.8",
|
"material-icons": "^1.10.8",
|
||||||
"nan": "^2.14.1",
|
"nan": "^2.14.1",
|
||||||
"ngx-avatars": "^1.4.1",
|
"ngx-avatars": "^1.4.1",
|
||||||
"ngx-file-drop": "^13.0.0",
|
"ngx-file-drop": "^15.0.0",
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^6.6.3",
|
||||||
"rxjs-compat": "^6.0.0-rc.0",
|
"rxjs-compat": "^6.6.7",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "~4.8.4",
|
"typescript": "~4.8.4",
|
||||||
"xliff-to-json": "^1.0.4",
|
"xliff-to-json": "^1.0.4",
|
||||||
@@ -60,21 +59,20 @@
|
|||||||
"@angular/language-service": "^15.0.1",
|
"@angular/language-service": "^15.0.1",
|
||||||
"@types/core-js": "^2.5.2",
|
"@types/core-js": "^2.5.2",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "^4.3.1",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||||
"@typescript-eslint/parser": "^4.29.0",
|
"@typescript-eslint/parser": "^4.29.0",
|
||||||
"ajv": "^7.2.4",
|
"ajv": "^7.2.4",
|
||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"electron": "^19.1.9",
|
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"jasmine-core": "~3.6.0",
|
"jasmine-core": "~3.6.0",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
"karma": "~6.3.16",
|
"karma": "~6.4.2",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-cli": "~1.0.1",
|
"karma-cli": "~1.0.1",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
"karma-jasmine": "~4.0.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"openapi-typescript-codegen": "^0.23.0",
|
"openapi-typescript-codegen": "^0.23.0",
|
||||||
"protractor": "~7.0.0",
|
"protractor": "~7.0.0",
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
14
src/api-types/models/TaskType.ts
Normal file
14
src/api-types/models/TaskType.ts
Normal 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',
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -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',
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<mat-icon>person</mat-icon>
|
<mat-icon>person</mat-icon>
|
||||||
<span i18n="Profile menu label">Profile</span>
|
<span i18n="Profile menu label">Profile</span>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="postsService.config && postsService.config.Downloader.use_youtubedl_archive" class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
|
<button class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
|
||||||
<mat-icon>topic</mat-icon>
|
<mat-icon>topic</mat-icon>
|
||||||
<span i18n="Archives menu label">Archives</span>
|
<span i18n="Archives menu label">Archives</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .ngx-file-drop__content {
|
:host ::ng-deep .ngx-file-drop__content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: -12px;
|
top: -12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
|
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''" [loading]="false"></app-unified-file-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
||||||
<mat-selection-list *ngIf="!normal_files_received">
|
<mat-selection-list *ngIf="!normal_files_received">
|
||||||
<mat-list-option *ngFor="let file of paged_data">
|
<mat-list-option *ngFor="let file of paged_data">
|
||||||
<content-loader class="list-ghosts" [primaryColor]="postsService.theme.ghost_primary" [secondaryColor]="postsService.theme.ghost_secondary" [width]="250" [height]="8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
|
<content-loader class="list-ghosts" [backgroundColor]="postsService.theme.ghost_primary" [foregroundColor]="postsService.theme.ghost_secondary" viewBox="0 0 250 8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
|
||||||
</mat-list-option>
|
</mat-list-option>
|
||||||
</mat-selection-list>
|
</mat-selection-list>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -76,8 +76,9 @@ export class RecentVideosComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(public postsService: PostsService, private router: Router) {
|
constructor(public postsService: PostsService, private router: Router) {
|
||||||
// get cached file count
|
// get cached file count
|
||||||
if (localStorage.getItem('cached_file_count')) {
|
const sub_id_appendix = this.sub_id ? `_${this.sub_id}` : ''
|
||||||
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
|
if (localStorage.getItem(`cached_file_count${sub_id_appendix}`)) {
|
||||||
|
this.cached_file_count = +localStorage.getItem(`cached_file_count${sub_id_appendix}`) <= 10 ? +localStorage.getItem(`cached_file_count${sub_id_appendix}`) : 10;
|
||||||
this.loading_files = Array(this.cached_file_count).fill(0);
|
this.loading_files = Array(this.cached_file_count).fill(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<ng-container i18n="Update binary to">Update binary to:</ng-container> {{element.data}}
|
<ng-container i18n="Update binary to">Update binary to:</ng-container> {{element.data}}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="element.key == 'delete_old_files'">
|
<ng-container *ngIf="element.key == 'delete_old_files'">
|
||||||
<ng-container i18n="Delete old files">Delete old files:</ng-container> {{element.data.uids.length}}
|
<ng-container i18n="Delete old files">Delete old files:</ng-container> {{element.data.files_to_remove.length}}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ mat-header-cell, mat-cell {
|
|||||||
border-radius: 16px 16px 16px 16px !important;
|
border-radius: 16px 16px 16px 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep mat-row {
|
:host ::ng-deep mat-row {
|
||||||
height: fit-content !important;
|
height: fit-content !important;
|
||||||
}
|
}
|
||||||
@@ -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!`);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<ng-container i18n="Auto-generated label" *ngIf="file_obj.auto">Auto-generated</ng-container>
|
<ng-container i18n="Auto-generated label" *ngIf="file_obj.auto">Auto-generated</ng-container>
|
||||||
<ng-container *ngIf="!file_obj.auto">{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</ng-container>
|
<ng-container *ngIf="!file_obj.auto">{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" [width]="250" [height]="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
|
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 250 30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
|
||||||
<!-- The context menu trigger must be kept above the "more info" menu -->
|
<!-- The context menu trigger must be kept above the "more info" menu -->
|
||||||
<div style="visibility: hidden; position: fixed"
|
<div style="visibility: hidden; position: fixed"
|
||||||
[style.left]="contextMenuPosition.x"
|
[style.left]="contextMenuPosition.x"
|
||||||
@@ -35,14 +35,9 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item>
|
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container></button>
|
||||||
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item>
|
|
||||||
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete and don't download again</ng-container>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
|
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
|
||||||
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and don't download again">Delete and don't download again</ng-container></button>
|
<button *ngIf="file_obj.sub_id || use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and don't download again">Delete and don't download again</ng-container></button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="is_playlist && !loading">
|
<ng-container *ngIf="is_playlist && !loading">
|
||||||
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
|
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
|
||||||
@@ -68,11 +63,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="loading" class="img-div">
|
<div *ngIf="loading" class="img-div">
|
||||||
<content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" [width]="100" [height]="55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader>
|
<content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 100 55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span *ngIf="!loading" [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }">{{card_size === 'large' && file_obj.uploader ? file_obj.uploader + ' - ' : ''}}<strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
|
<span *ngIf="!loading" [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }">{{card_size === 'large' && file_obj.uploader ? file_obj.uploader + ' - ' : ''}}<strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
|
||||||
<span *ngIf="loading" class="title-loading"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" [width]="250" [height]="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span>
|
<span *ngIf="loading" class="title-loading"><content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 250 30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span>
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -171,6 +171,6 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep.mat-mdc-menu-panel {
|
:host ::ng-deep.mat-mdc-menu-panel {
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
}
|
}
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
top: -12px;
|
top: -12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep.mat-menu-panel {
|
:host ::ng-deep.mat-menu-panel {
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep.mdc-list-item__primary-text {
|
:host ::ng-deep.mdc-list-item__primary-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -31,9 +31,11 @@
|
|||||||
<mat-form-field class="value-input">
|
<mat-form-field class="value-input">
|
||||||
<input matInput [(ngModel)]="rule['value']">
|
<input matInput [(ngModel)]="rule['value']">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
|
<span class="rule-buttons">
|
||||||
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
|
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
|
||||||
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
|
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
|
||||||
|
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
|
||||||
|
</span>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.operator-select {
|
.operator-select {
|
||||||
width: 55px;
|
width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-select {
|
.property-select {
|
||||||
@@ -14,3 +14,16 @@
|
|||||||
.value-input {
|
.value-input {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep.mdc-list-item {
|
||||||
|
height: 75px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep.mdc-list-item__content {
|
||||||
|
pointer-events: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-buttons {
|
||||||
|
position: relative;
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
@@ -37,7 +37,8 @@
|
|||||||
<input [(ngModel)]="new_file.thumbnailURL" matInput [disabled]="!editing || new_file.thumbnailPath">
|
<input [(ngModel)]="new_file.thumbnailURL" matInput [disabled]="!editing || new_file.thumbnailPath">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field *ngIf="initialized && postsService.categories" class="info-field">
|
<mat-form-field *ngIf="initialized && postsService.categories" class="info-field">
|
||||||
<mat-select placeholder="Category" i18n-placeholder="Category" [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
|
<mat-label i18n="Category">Category</mat-label>
|
||||||
|
<mat-select [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
|
||||||
<mat-option [value]="{}">
|
<mat-option [value]="{}">
|
||||||
N/A
|
N/A
|
||||||
</mat-option>
|
</mat-option>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit, Inject } from '@angular/core';
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
import filesize from 'filesize';
|
import { filesize } from 'filesize';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { Category, DatabaseFile } from 'api-types';
|
import { Category, DatabaseFile } from 'api-types';
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export class MainComponent implements OnInit {
|
|||||||
allowQualitySelect = false;
|
allowQualitySelect = false;
|
||||||
downloadOnlyMode = false;
|
downloadOnlyMode = false;
|
||||||
forceAutoplay = false;
|
forceAutoplay = false;
|
||||||
use_youtubedl_archive = false;
|
|
||||||
globalCustomArgs = null;
|
globalCustomArgs = null;
|
||||||
allowAdvancedDownload = false;
|
allowAdvancedDownload = false;
|
||||||
useDefaultDownloadingAgent = true;
|
useDefaultDownloadingAgent = true;
|
||||||
@@ -188,7 +187,6 @@ export class MainComponent implements OnInit {
|
|||||||
&& this.postsService.hasPermission('filemanager');
|
&& this.postsService.hasPermission('filemanager');
|
||||||
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
|
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
|
||||||
this.forceAutoplay = this.postsService.config['Extra']['force_autoplay'];
|
this.forceAutoplay = this.postsService.config['Extra']['force_autoplay'];
|
||||||
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
|
|
||||||
this.globalCustomArgs = this.postsService.config['Downloader']['custom_args'];
|
this.globalCustomArgs = this.postsService.config['Downloader']['custom_args'];
|
||||||
this.youtubeSearchEnabled = this.postsService.config['API'] && this.postsService.config['API']['use_youtube_API'] &&
|
this.youtubeSearchEnabled = this.postsService.config['API'] && this.postsService.config['API']['use_youtube_API'] &&
|
||||||
this.postsService.config['API']['youtube_API_key'];
|
this.postsService.config['API']['youtube_API_key'];
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="db_file || playlist[currentIndex]"></ng-container>
|
<ng-container *ngIf="db_file || playlist[currentIndex]"></ng-container>
|
||||||
<button (click)="openFileInfoDialog()" *ngIf="db_file || db_playlist" mat-icon-button><mat-icon>info</mat-icon></button>
|
<button (click)="openFileInfoDialog()" *ngIf="db_file || db_playlist" mat-icon-button><mat-icon>info</mat-icon></button>
|
||||||
<button *ngIf="db_file && db_file.url.includes('twitch.tv') && postsService['config']['API']['use_twitch_API']" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
|
<button *ngIf="db_file && db_file.url.includes('twitch.tv')" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
<app-concurrent-stream *ngIf="db_file && api && postsService.config" (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
|
<app-concurrent-stream *ngIf="db_file && api && postsService.config" (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
|
||||||
|
|
||||||
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists'] && postsService['config']['API']['use_twitch_API']">
|
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists']">
|
||||||
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv')">
|
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv')">
|
||||||
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
|
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router';
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import * as Fingerprint2 from 'fingerprintjs2';
|
|
||||||
import {
|
import {
|
||||||
ChangeRolePermissionsRequest,
|
ChangeRolePermissionsRequest,
|
||||||
ChangeUserPermissionsRequest,
|
ChangeUserPermissionsRequest,
|
||||||
@@ -113,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';
|
||||||
@@ -131,7 +131,6 @@ export class PostsService implements CanActivate {
|
|||||||
|
|
||||||
// auth
|
// auth
|
||||||
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
||||||
session_id = null;
|
|
||||||
httpOptions: {
|
httpOptions: {
|
||||||
params: HttpParams
|
params: HttpParams
|
||||||
};
|
};
|
||||||
@@ -187,12 +186,6 @@ export class PostsService implements CanActivate {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
Fingerprint2.get(components => {
|
|
||||||
// set identity as user id doesn't necessarily exist
|
|
||||||
this.session_id = Fingerprint2.x64hash128(components.map(function (pair) { return pair.value; }).join(), 31);
|
|
||||||
this.httpOptions.params = this.httpOptions.params.set('sessionID', this.session_id);
|
|
||||||
});
|
|
||||||
|
|
||||||
const redirect_not_required = window.location.href.includes('/player') || window.location.href.includes('/login');
|
const redirect_not_required = window.location.href.includes('/player') || window.location.href.includes('/login');
|
||||||
|
|
||||||
// get config
|
// get config
|
||||||
@@ -648,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);
|
||||||
}
|
}
|
||||||
@@ -742,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();
|
||||||
@@ -796,7 +789,7 @@ export class PostsService implements CanActivate {
|
|||||||
|
|
||||||
resetHttpParams() {
|
resetHttpParams() {
|
||||||
// resets http params
|
// resets http params
|
||||||
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
|
this.http_params = `apiKey=${this.auth_token}`
|
||||||
|
|
||||||
this.httpOptions = {
|
this.httpOptions = {
|
||||||
params: new HttpParams({
|
params: new HttpParams({
|
||||||
|
|||||||
@@ -269,25 +269,9 @@
|
|||||||
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
|
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-1">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_twitch_API']"><ng-container i18n="Use Twitch API setting">Use Twitch API</ng-container></mat-checkbox>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="new_config['API']['use_twitch_API']" class="col-12 mt-1">
|
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
|
||||||
<mat-form-field class="text-field" color="accent">
|
|
||||||
<mat-label i18n="Twitch Client ID">Twitch Client ID</mat-label>
|
|
||||||
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_ID']" matInput required>
|
|
||||||
<mat-hint><a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch Client ID setting hint">Generating an ID/secret is easy!</ng-container></a></mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 mt-2">
|
|
||||||
<mat-form-field class="text-field" color="accent">
|
|
||||||
<mat-label i18n="Twitch Client Secret">Twitch Client Secret</mat-label>
|
|
||||||
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_secret']" matInput required>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 mt-2">
|
<div class="col-12 mt-2">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,6 +385,20 @@
|
|||||||
<mat-hint>Place endpoint URL here to integrate with services like Zapier and Automatisch.</mat-hint>
|
<mat-hint>Place endpoint URL here to integrate with services like Zapier and Automatisch.</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 mb-2 mt-3">
|
||||||
|
<mat-form-field class="text-field" color="accent">
|
||||||
|
<mat-label i18n="Discord Webhook URL">Discord Webhook URL</mat-label>
|
||||||
|
<input placeholder="https://discord.com/api/webhooks/<webhook_id>/<webhook_token>" [(ngModel)]="new_config['API']['discord_webhook_URL']" matInput>
|
||||||
|
<mat-hint><a target="_blank" href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"><ng-container i18n="Discord API setting hint">See docs here.</ng-container></a></mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-2 mt-3">
|
||||||
|
<mat-form-field class="text-field" color="accent">
|
||||||
|
<mat-label i18n="Slack Webhook URL">Slack Webhook URL</mat-label>
|
||||||
|
<input placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" [(ngModel)]="new_config['API']['slack_webhook_URL']" matInput>
|
||||||
|
<mat-hint><a target="_blank" href="https://api.slack.com/messaging/webhooks"><ng-container i18n="Slack API setting hint">See docs here.</ng-container></a></mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-checkbox color="accent" [disabled]="!new_config['Extra']['enable_notifications']" [(ngModel)]="new_config['API']['use_ntfy_API']"><ng-container i18n="Use ntfy API setting">Use ntfy API</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [disabled]="!new_config['Extra']['enable_notifications']" [(ngModel)]="new_config['API']['use_ntfy_API']"><ng-container i18n="Use ntfy API setting">Use ntfy API</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .mat-mdc-tab-body {
|
:host ::ng-deep .mat-mdc-tab-body {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
latestGithubRelease = null;
|
latestGithubRelease = null;
|
||||||
CURRENT_VERSION = CURRENT_VERSION
|
CURRENT_VERSION = CURRENT_VERSION
|
||||||
|
|
||||||
tabs = ['main', 'downloader', 'extra', 'database', 'advanced', 'users', 'logs'];
|
tabs = ['main', 'downloader', 'extra', 'database', 'notifications', 'advanced', 'users', 'logs'];
|
||||||
tabIndex = 0;
|
tabIndex = 0;
|
||||||
|
|
||||||
INDEX_TO_TAB = Object.assign({}, this.tabs);
|
INDEX_TO_TAB = Object.assign({}, this.tabs);
|
||||||
|
|||||||
@@ -4184,6 +4184,612 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">Restore button</note>
|
<note priority="1" from="description">Restore button</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html">
|
||||||
|
<source>Archives</source>
|
||||||
|
<target state="translated">Archive</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">Filter</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="51a161ce175abcd44f6c6cbd0e996681bf553ac3" datatype="html">
|
||||||
|
<source>Delete selected</source>
|
||||||
|
<target state="translated">Ausgewählte löschen</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">Archiv herunterladen</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">Kein</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">Hochladen</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">Archiv erfolgreich importiert!</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">Archive löschen</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">Löschen</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="019d4bd6a5690f0cfa0ecf346a4e6bf7f0d8debb" datatype="html">
|
||||||
|
<source>Remove</source>
|
||||||
|
<target state="translated">Entfernen</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">Herunterladen abgeschlossen</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">Herunterladen fehlgeschlagen</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">Aufgabe abgeschlossen</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="8564202903947049539" datatype="html">
|
||||||
|
<source>Play</source>
|
||||||
|
<target state="translated">Wiedergabe</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="5a105e7bd7e7db6ea211fe950fc9f317379acceb" datatype="html">
|
||||||
|
<source>No notifications available</source>
|
||||||
|
<target state="translated">Keine Benachrichtigungen verfügbar</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="5709555629190115111" datatype="html">
|
||||||
|
<source>View task</source>
|
||||||
|
<target state="translated">Aufgabe ansehen</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="6876310993601590130" datatype="html">
|
||||||
|
<source>Download completed</source>
|
||||||
|
<target state="translated">Herunterladen abgeschlossen</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="4578192247039196794" datatype="html">
|
||||||
|
<source>Task</source>
|
||||||
|
<target state="translated">Aufgabe</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="7911845622864460134" datatype="html">
|
||||||
|
<source>Video only</source>
|
||||||
|
<target state="translated">Nur 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="6437411876967154040" datatype="html">
|
||||||
|
<source>Audio only</source>
|
||||||
|
<target state="translated">Nur 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="6268070779441507380" datatype="html">
|
||||||
|
<source>Download Date</source>
|
||||||
|
<target state="translated">Herunterladedatum</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="cdf5297d8d080a78e8b10debc5c38b7845a3cbe7" datatype="html">
|
||||||
|
<source>Do not ask for confirmation</source>
|
||||||
|
<target state="translated">Nicht nach einer Bestätigung fragen</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="7b4585a9072f3c1292972c14a3d0e14978fbfc9c" datatype="html">
|
||||||
|
<source>Delete old files:</source>
|
||||||
|
<target state="translated">Alte Dateien löschen:</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="9176960997786930103" datatype="html">
|
||||||
|
<source>Error for: <x id="PH" equiv-text="task['title']"/></source>
|
||||||
|
<target state="translated">Fehler für: <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">Favorit</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="5ac5a0e5ffe8e5623b40696f4c2403c17349271f" datatype="html">
|
||||||
|
<source>Sidepanel mode</source>
|
||||||
|
<target state="translated">Seitenleisten-Modus</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="1a9b415816364f554ee411020e65219092655271" datatype="html">
|
||||||
|
<source>Title filter</source>
|
||||||
|
<target state="translated">Titelfilter</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">Unterstützt reguläre Ausdrücke</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">Benutzer</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="d57c023a4cf63b2f12c10328c15b636ff18929aa" datatype="html">
|
||||||
|
<source>Best</source>
|
||||||
|
<target state="translated">Beste</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">Automatische Wiedergabe erzwingen</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="8bcabdf6b16cad0313a86c7e940c5e3ad7f9f8ab" datatype="html">
|
||||||
|
<source>Notifications</source>
|
||||||
|
<target state="translated">Benachrichtigungen</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">Benachrichtigungen aktivieren</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">Alle Benachrichtigungen aktivieren</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">Erlaubte Benachrichtigungsarten</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">Herunterladen abgeschlossen</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="2361a4f76caaa4574803fbcdca8b0a47c91cc7ed" datatype="html">
|
||||||
|
<source>Task finished</source>
|
||||||
|
<target state="translated">Aufgabe abgeschlossen</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="38992954440d6afb54aeb58af12ca0123ee5e26e" datatype="html">
|
||||||
|
<source>Use Telegram API</source>
|
||||||
|
<target state="translated">Telegram-API verwenden</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="eeb0ba2e4743901d8f5eebd9a3529aa1f236c608" datatype="html">
|
||||||
|
<source>Create bot here.</source>
|
||||||
|
<target state="translated">Bot hier erstellen.</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="a4ed8eba1e057e67d5c2d87b52230f182b3dae4e" datatype="html">
|
||||||
|
<source>Restart required.</source>
|
||||||
|
<target state="translated">Neustart erforderlich.</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">Kategorie löschen</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="7332320960988475089" datatype="html">
|
||||||
|
<source>Successfully deleted <x id="category name" equiv-text="category['name']"/>!</source>
|
||||||
|
<target state="translated"><x id="category name" equiv-text="category['name']"/> erfolgreich gelöscht!</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="8c6e24eab969d9f63a8a0e9d617aee3b99e28ae6" datatype="html">
|
||||||
|
<source>Play all</source>
|
||||||
|
<target state="translated">Alle wiedergeben</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">ZIP herunterladen</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">Abonnement hinzufügen</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="3533826530554274875" datatype="html">
|
||||||
|
<source>Upload Date</source>
|
||||||
|
<target state="translated">Hochladedatum</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">Name</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">Dateigröße</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="ea2b65121b93921fe54692025da9b9e3ce779ad5" datatype="html">
|
||||||
|
<source>Task settings - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></source>
|
||||||
|
<target state="translated">Aufgabeneinstellungen - <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="7410432243549869948" datatype="html">
|
||||||
|
<source>Duration</source>
|
||||||
|
<target state="translated">Dauer</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="c65dd978b3c7566551c0ebefb234c2d41942b847" datatype="html">
|
||||||
|
<source>Delete files older than</source>
|
||||||
|
<target state="translated">Löschen von Dateien, die älter sind als</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="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
|
||||||
|
<source>ID</source>
|
||||||
|
<target state="translated">Kennung</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">Archive leer</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="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">Möchten Sie <x id="selected archives amount" equiv-text="this.selection.selected.length"/> Archiv(e) löschen?</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="2525880134753073592" datatype="html">
|
||||||
|
<source>Successfully deleted archive items!</source>
|
||||||
|
<target state="translated">Archivelemente erfolgreich gelöscht!</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">Fehler beim Löschen von Archivelementen!</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="8643601595923420698" datatype="html">
|
||||||
|
<source>Retry download</source>
|
||||||
|
<target state="translated">Herunterladen erneut versuchen</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">Fehler ansehen</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">Herunterladefehler</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="5000203534763292992" datatype="html">
|
||||||
|
<source>Download restarted!</source>
|
||||||
|
<target state="translated">Herunterladen neu gestartet!</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="1f2809e6a99d511fdb6eaf041d785fe54d0680cc" datatype="html">
|
||||||
|
<source>File card size</source>
|
||||||
|
<target state="translated">Dateikartengröße</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="c35ef0f03a863d33b04aae6807f140397a50f491" datatype="html">
|
||||||
|
<source>Generate RSS URL</source>
|
||||||
|
<target state="translated">RSS-URL generieren</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="1091872159779006651" datatype="html">
|
||||||
|
<source>You must input a time!</source>
|
||||||
|
<target state="translated">Sie müssen eine Zeit eingeben!</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="fd467148a18f0921c10d116d4e0174fe29452be4" datatype="html">
|
||||||
|
<source>See documentation here.</source>
|
||||||
|
<target state="translated">Siehe Dokumentation hier.</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="61d6b5fa4311b1c617b66dad72496f9dd43b07b4" datatype="html">
|
||||||
|
<source>Be careful enabling this with multi-user mode! User data may be exposed.</source>
|
||||||
|
<target state="translated">Seien Sie vorsichtig, wenn Sie diese Funktion im Mehrbenutzermodus aktivieren! Benutzerdaten können offengelegt werden.</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="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html">
|
||||||
|
<source>Download error</source>
|
||||||
|
<target state="translated">Herunterladefehler</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>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -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'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'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'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't include extension.</source>
|
<source>Path is relative to the config download path. Don'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's settings to set the frontend URL.</source>
|
<source>You must manually load the extension and modify the extension'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's settings to set the frontend URL.</source>
|
<source>Not much is required other than changing the extension'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're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark.</source>
|
<source>Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'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 'audio only' bookmarklet</source>
|
<source>Generate 'audio only' 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
4399
src/assets/i18n/messages.et.xlf
Normal file
4399
src/assets/i18n/messages.et.xlf
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
|||||||
@@ -1432,7 +1432,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
|
<trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
|
||||||
<source>Profile</source>
|
<source>Profile</source>
|
||||||
<target>个人资料</target>
|
<target state="translated">资料</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">app/app.component.html</context>
|
<context context-type="sourcefile">app/app.component.html</context>
|
||||||
<context context-type="linenumber">19</context>
|
<context context-type="linenumber">19</context>
|
||||||
@@ -3033,7 +3033,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4e1fdb6039c7c6b7630ed70d6d20eb0c9db7d342" datatype="html">
|
<trans-unit id="4e1fdb6039c7c6b7630ed70d6d20eb0c9db7d342" datatype="html">
|
||||||
<source>Video only</source>
|
<source>Video only</source>
|
||||||
<target state="translated">只视频</target>
|
<target state="translated">仅视频</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
|
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
|
||||||
<context context-type="linenumber">55</context>
|
<context context-type="linenumber">55</context>
|
||||||
@@ -4011,8 +4011,8 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
</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">已创建播放列表。</target>
|
<target state="translated">成功创建播放列表!</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>
|
||||||
@@ -4191,6 +4191,830 @@
|
|||||||
<context context-type="linenumber">299</context>
|
<context context-type="linenumber">299</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html">
|
||||||
|
<source>Archives</source>
|
||||||
|
<target state="translated">存档</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">筛选</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="28da11220a3377ddce3c7948825d33101f142782" datatype="html">
|
||||||
|
<source>Extractor</source>
|
||||||
|
<target state="translated">提取</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="c150a30bbbdb175b4d08820196a9acb66b167653" datatype="html">
|
||||||
|
<source>Archives empty</source>
|
||||||
|
<target state="translated">存档为空</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">删除所选内容</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">下载存档</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">无</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">上传</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">视频</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="3159807825117518005" datatype="html">
|
||||||
|
<source>Delete archives</source>
|
||||||
|
<target state="translated">删除存档</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="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">是否要删除 <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="8224301330941792118" datatype="html">
|
||||||
|
<source>Failed to delete archive items!</source>
|
||||||
|
<target state="translated">无法删除存档项目!</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">移除</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">下载完成</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">下载失败</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">任务完成</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="8564202903947049539" datatype="html">
|
||||||
|
<source>Play</source>
|
||||||
|
<target state="translated">播放</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">重试下载</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">查看错误</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="5709555629190115111" datatype="html">
|
||||||
|
<source>View task</source>
|
||||||
|
<target state="translated">查看任务</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">没有通知</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">下载完成</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="1879058637439215882" datatype="html">
|
||||||
|
<source>Download error</source>
|
||||||
|
<target state="translated">下载错误</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="4578192247039196794" datatype="html">
|
||||||
|
<source>Task</source>
|
||||||
|
<target state="translated">任务</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">下载已重新启动!</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="7911845622864460134" datatype="html">
|
||||||
|
<source>Video only</source>
|
||||||
|
<target state="translated">仅视频</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="6437411876967154040" datatype="html">
|
||||||
|
<source>Audio only</source>
|
||||||
|
<target state="translated">仅音频</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">收藏</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="3533826530554274875" datatype="html">
|
||||||
|
<source>Upload Date</source>
|
||||||
|
<target state="translated">上传日期</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="6268070779441507380" datatype="html">
|
||||||
|
<source>Download Date</source>
|
||||||
|
<target state="translated">下载日期</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="8953033926734869941" datatype="html">
|
||||||
|
<source>Name</source>
|
||||||
|
<target state="translated">名称</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="c65dd978b3c7566551c0ebefb234c2d41942b847" datatype="html">
|
||||||
|
<source>Delete files older than</source>
|
||||||
|
<target state="translated">删除早于</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">将所有文件列入黑名单</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">黑名单删除的订阅文件</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">不要求确认</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="7b4585a9072f3c1292972c14a3d0e14978fbfc9c" datatype="html">
|
||||||
|
<source>Delete old files:</source>
|
||||||
|
<target state="translated">删除旧文件:</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="9176960997786930103" datatype="html">
|
||||||
|
<source>Error for: <x id="PH" equiv-text="task['title']"/></source>
|
||||||
|
<target state="translated">错误: <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="5ac5a0e5ffe8e5623b40696f4c2403c17349271f" datatype="html">
|
||||||
|
<source>Sidepanel mode</source>
|
||||||
|
<target state="translated">侧板模式</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">文件卡大小</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">参数</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">生成 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="c2faa86201eab08b5b39b5437f96ab9432e125e7" datatype="html">
|
||||||
|
<source>Item limit</source>
|
||||||
|
<target state="translated">项目限制</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">收藏</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">取消订阅 <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="784837056777689544" datatype="html">
|
||||||
|
<source>Would you like to unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/>?</source>
|
||||||
|
<target state="translated">是否要取消订阅 <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="1698114086921246480" datatype="html">
|
||||||
|
<source>Unsubscribe</source>
|
||||||
|
<target state="translated">取消订阅</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">你必须输入一个时间!</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">最佳</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">强制自动播放</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">启用 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">使用多用户模式启用此功能时要小心!用户数据可能会暴露。</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">请看这里的文档。</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">通知</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="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html">
|
||||||
|
<source>Download error</source>
|
||||||
|
<target state="translated">下载错误</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="c40370dc182b5e4333828b70f7478bde58bb5cfe" datatype="html">
|
||||||
|
<source>Enable notifications</source>
|
||||||
|
<target state="translated">启用通知</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">启用所有通知</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">允许的通知类型</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">下载完成</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="2361a4f76caaa4574803fbcdca8b0a47c91cc7ed" datatype="html">
|
||||||
|
<source>Task finished</source>
|
||||||
|
<target state="translated">任务完成</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">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">使用 ntfy API</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="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html">
|
||||||
|
<source>See docs here.</source>
|
||||||
|
<target state="translated">请看这里的文档。</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="55f559d6f666b945479f534b0c182f70cd0a8a69" datatype="html">
|
||||||
|
<source>Gotify server URL</source>
|
||||||
|
<target state="translated">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">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">使用 Telegram API</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">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">在此处创建机器人。</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">Telegram 聊天 ID</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">如何获取聊天 ID?</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">需要重新启动。</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">删除类别</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">您要删除 <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="7332320960988475089" datatype="html">
|
||||||
|
<source>Successfully deleted <x id="category name" equiv-text="category['name']"/>!</source>
|
||||||
|
<target state="translated">已成功删除 <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="3371159074051387771" datatype="html">
|
||||||
|
<source>Failed to delete <x id="category name" equiv-text="category['name']"/>!</source>
|
||||||
|
<target state="translated">删除 <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">全部播放</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">下载压缩包 (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">添加订阅</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="8953483585652369683" datatype="html">
|
||||||
|
<source>Archive successfully imported!</source>
|
||||||
|
<target state="translated">存档成功导入!</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="ea2b65121b93921fe54692025da9b9e3ce779ad5" datatype="html">
|
||||||
|
<source>Task settings - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></source>
|
||||||
|
<target state="translated">任务设置 - <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="06f503e492d6dbcf59e7b9c412ca86913d718689" datatype="html">
|
||||||
|
<source>ntfy topic URL</source>
|
||||||
|
<target state="translated">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="347407180135731058" datatype="html">
|
||||||
|
<source>Audio</source>
|
||||||
|
<target state="translated">音频</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="7022070615528435141" datatype="html">
|
||||||
|
<source>Delete</source>
|
||||||
|
<target state="translated">删除</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">已成功删除存档项目!</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="2492098975665776610" datatype="html">
|
||||||
|
<source>File Size</source>
|
||||||
|
<target state="translated">文件大小</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">期间</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="11a0771f88158a540a54e0e4ec5d25733d65fc0e" datatype="html">
|
||||||
|
<source>Favorite</source>
|
||||||
|
<target state="translated">喜欢</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">不喜欢</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="1a9b415816364f554ee411020e65219092655271" datatype="html">
|
||||||
|
<source>Title filter</source>
|
||||||
|
<target state="translated">标题过滤</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="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
|
||||||
|
<source>User</source>
|
||||||
|
<target state="translated">用户</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="9aa62bf1a535a97a4d752bbc5cf1c31af0f0c1f7" datatype="html">
|
||||||
|
<source>Supports regex</source>
|
||||||
|
<target state="translated">支持正则表达式</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="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html">
|
||||||
|
<source>Use gotify API</source>
|
||||||
|
<target state="translated">使用 gotify API</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>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Reference in New Issue
Block a user