mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-11 15:20:56 +03:00
Compare commits
122 Commits
docker-fix
...
ui-updates
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ab8af55f4 | ||
|
|
d18fe70002 | ||
|
|
a6478a50f2 | ||
|
|
b8eb639a59 | ||
|
|
3ab2420c7e | ||
|
|
6580c712cf | ||
|
|
bd100544d7 | ||
|
|
461025d859 | ||
|
|
2568514357 | ||
|
|
03359940ff | ||
|
|
f57263d137 | ||
|
|
84c2b2769b | ||
|
|
e145c9c992 | ||
|
|
2adbc0a02c | ||
|
|
fe95f04c18 | ||
|
|
9b3816afce | ||
|
|
07874d9241 | ||
|
|
9fa1aab1e5 | ||
|
|
80b41af620 | ||
|
|
ab5d8dc5ca | ||
|
|
4b55c39f39 | ||
|
|
3ca296f195 | ||
|
|
d4fa640f0f | ||
|
|
427eecf214 | ||
|
|
4f54e408a5 | ||
|
|
9e481bbd5f | ||
|
|
78b29a76b8 | ||
|
|
0342d18f76 | ||
|
|
70754c580c | ||
|
|
e58b0b8638 | ||
|
|
df8f8070ca | ||
|
|
0b8ca31594 | ||
|
|
658a76dc1c | ||
|
|
f363ec5db6 | ||
|
|
f36d675abf | ||
|
|
be74377a08 | ||
|
|
808c7e2112 | ||
|
|
d6f39d37b5 | ||
|
|
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"]
|
||||||
|
|||||||
@@ -2742,7 +2742,7 @@ components:
|
|||||||
error:
|
error:
|
||||||
type: string
|
type: string
|
||||||
schedule:
|
schedule:
|
||||||
type: object
|
$ref: '#/components/schemas/Schedule'
|
||||||
options:
|
options:
|
||||||
type: object
|
type: object
|
||||||
Schedule:
|
Schedule:
|
||||||
@@ -2877,6 +2877,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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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!'); }
|
||||||
@@ -723,7 +726,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 +937,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 +1103,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 +1231,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 +1319,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 +1332,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 +1376,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 +1384,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 +1414,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 +1446,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 +1472,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 +1489,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 +1636,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;
|
||||||
@@ -2031,7 +2034,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 +2043,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 +2051,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 +2059,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 +2084,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",
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -354,4 +350,6 @@ const YTDL_ARGS_WITH_VALUES = [
|
|||||||
// 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"
|
||||||
|
|||||||
@@ -92,7 +92,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});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,8 +199,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;
|
||||||
@@ -229,13 +237,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);
|
||||||
@@ -354,6 +369,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 +430,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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 fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
@@ -20,7 +21,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
|
||||||
},
|
},
|
||||||
@@ -101,7 +102,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 +116,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,7 +260,7 @@ 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']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'});
|
||||||
@@ -330,11 +351,13 @@ describe('Multi User', async function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -352,12 +375,12 @@ describe('Multi User', async function() {
|
|||||||
});
|
});
|
||||||
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 +408,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 +511,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 +609,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 +727,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,23 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
// 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",
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
YoutubeDLMaterial: any;
|
YoutubeDLMaterial: Record<string, any>;
|
||||||
};
|
};
|
||||||
@@ -26,5 +26,5 @@ export type Download = {
|
|||||||
user_uid?: string;
|
user_uid?: string;
|
||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
sub_name?: string;
|
sub_name?: string;
|
||||||
prefetched_info?: any;
|
prefetched_info?: Record<string, any>;
|
||||||
};
|
};
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
export type GetFileFormatsResponse = {
|
export type GetFileFormatsResponse = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
result: {
|
result: {
|
||||||
formats?: Array<any>;
|
formats?: Array<Record<string, any>>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -6,5 +6,5 @@ import type { Subscription } from './Subscription';
|
|||||||
|
|
||||||
export type GetSubscriptionResponse = {
|
export type GetSubscriptionResponse = {
|
||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
files: Array<any>;
|
files: Array<Record<string, any>>;
|
||||||
};
|
};
|
||||||
@@ -11,6 +11,6 @@ export type Notification = {
|
|||||||
user_uid?: string;
|
user_uid?: string;
|
||||||
action?: Array<NotificationAction>;
|
action?: Array<NotificationAction>;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
data?: any;
|
data?: Record<string, any>;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
@@ -15,5 +15,5 @@ export type Subscription = {
|
|||||||
timerange?: string;
|
timerange?: string;
|
||||||
custom_args?: string;
|
custom_args?: string;
|
||||||
custom_output?: string;
|
custom_output?: string;
|
||||||
videos: Array<any>;
|
videos: Array<Record<string, any>>;
|
||||||
};
|
};
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import type { Schedule } from './Schedule';
|
||||||
|
|
||||||
export type Task = {
|
export type Task = {
|
||||||
key: string;
|
key: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -9,8 +11,8 @@ export type Task = {
|
|||||||
last_confirmed: number;
|
last_confirmed: number;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
confirming: boolean;
|
confirming: boolean;
|
||||||
data: any;
|
data: Record<string, any>;
|
||||||
error: string;
|
error: string;
|
||||||
schedule: any;
|
schedule: Schedule;
|
||||||
options?: any;
|
options?: Record<string, any>;
|
||||||
};
|
};
|
||||||
@@ -10,5 +10,5 @@ export type UpdateFileRequest = {
|
|||||||
/**
|
/**
|
||||||
* Object with fields to update as keys and their new values
|
* Object with fields to update as keys and their new values
|
||||||
*/
|
*/
|
||||||
change_obj: any;
|
change_obj: Record<string, any>;
|
||||||
};
|
};
|
||||||
@@ -4,5 +4,5 @@
|
|||||||
|
|
||||||
export type UpdateTaskDataRequest = {
|
export type UpdateTaskDataRequest = {
|
||||||
task_key: string;
|
task_key: string;
|
||||||
new_data: any;
|
new_data: Record<string, any>;
|
||||||
};
|
};
|
||||||
@@ -4,5 +4,5 @@
|
|||||||
|
|
||||||
export type UpdateTaskOptionsRequest = {
|
export type UpdateTaskOptionsRequest = {
|
||||||
task_key: string;
|
task_key: string;
|
||||||
new_options: any;
|
new_options: Record<string, any>;
|
||||||
};
|
};
|
||||||
@@ -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',
|
||||||
}
|
}
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
</mat-menu>
|
</mat-menu>
|
||||||
<button [matMenuTriggerFor]="menuSettings" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
<button [matMenuTriggerFor]="menuSettings" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||||
<mat-menu #menuSettings="matMenu">
|
<mat-menu #menuSettings="matMenu">
|
||||||
<button class="top-menu-button" (click)="openProfileDialog()" *ngIf="postsService.isLoggedIn" mat-menu-item>
|
<button class="top-menu-button" (click)="openProfileDialog()" mat-menu-item>
|
||||||
<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 *ngIf="!postsService.config?.Advanced.multi_user_mode || postsService.isLoggedIn" 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.');
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { MatBadgeModule } from '@angular/material/badge';
|
|||||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||||
|
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
@@ -189,6 +190,7 @@ registerLocaleData(es, 'es');
|
|||||||
DragDropModule,
|
DragDropModule,
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
TextFieldModule,
|
TextFieldModule,
|
||||||
|
ScrollingModule,
|
||||||
NgxFileDropModule,
|
NgxFileDropModule,
|
||||||
AvatarModule,
|
AvatarModule,
|
||||||
ContentLoaderModule,
|
ContentLoaderModule,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
<!-- Title Column -->
|
<!-- Title Column -->
|
||||||
<ng-container matColumnDef="title">
|
<ng-container matColumnDef="title">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header style="flex: 2"> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element" style="flex: 2">
|
||||||
<span class="one-line" [matTooltip]="element.title ? element.title : null">
|
<span class="one-line" [matTooltip]="element.title ? element.title : null">
|
||||||
{{element.title}}
|
{{element.title}}
|
||||||
</span>
|
</span>
|
||||||
@@ -31,41 +31,47 @@
|
|||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Stage Column -->
|
|
||||||
<ng-container matColumnDef="step_index">
|
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Stage">Stage</ng-container> </mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let element"> {{STEP_INDEX_TO_LABEL[element.step_index]}} </mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Progress Column -->
|
<!-- Progress Column -->
|
||||||
<ng-container matColumnDef="percent_complete">
|
<ng-container matColumnDef="percent_complete">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Progress">Progress</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Progress">Progress</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element">
|
||||||
|
<ng-container *ngIf="!element.error && element.step_index !== 2">
|
||||||
|
{{STEP_INDEX_TO_LABEL[element.step_index]}}
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!element.error && element.step_index === 2">
|
||||||
<ng-container *ngIf="element.percent_complete">
|
<ng-container *ngIf="element.percent_complete">
|
||||||
{{+(element.percent_complete) > 100 ? '100' : element.percent_complete}}%
|
{{+(element.percent_complete) > 100 ? '100' : element.percent_complete}}%
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!element.percent_complete">
|
<ng-container *ngIf="!element.percent_complete">
|
||||||
N/A
|
N/A
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="element.error" i18n="Error">Error</ng-container>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Actions Column -->
|
<!-- Actions Column -->
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef [ngStyle]="{flex: actionsFlex}"> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element" [ngStyle]="{flex: actionsFlex}">
|
||||||
<div>
|
<div *ngIf="!minimizeButtons">
|
||||||
<ng-container *ngIf="!element.finished">
|
<ng-container *ngFor="let downloadAction of downloadActions">
|
||||||
<button (click)="pauseDownload(element.uid)" *ngIf="!element.paused || !element.finished_step" [disabled]="element.paused && !element.finished_step" mat-icon-button matTooltip="Pause" i18n-matTooltip="Pause"><mat-spinner [diameter]="28" *ngIf="element.paused && !element.finished_step" class="icon-button-spinner"></mat-spinner><mat-icon>pause</mat-icon></button>
|
<span class="button-span">
|
||||||
<button (click)="resumeDownload(element.uid)" *ngIf="element.paused && element.finished_step" mat-icon-button matTooltip="Resume" i18n-matTooltip="Resume"><mat-icon>play_arrow</mat-icon></button>
|
<mat-spinner [diameter]="28" *ngIf="downloadAction.loading && downloadAction.loading(element)" class="icon-button-spinner"></mat-spinner>
|
||||||
<button *ngIf="false && !element.paused" (click)="cancelDownload(element.uid)" mat-icon-button matTooltip="Cancel" i18n-matTooltip="Cancel"><mat-icon>cancel</mat-icon></button>
|
<button *ngIf="downloadAction.show(element)" (click)="downloadAction.action(element)" [disabled]="downloadAction.loading && downloadAction.loading(element)" [matTooltip]="downloadAction.tooltip" mat-icon-button><mat-icon>{{downloadAction.icon}}</mat-icon></button>
|
||||||
|
</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="element.finished">
|
</div>
|
||||||
<button *ngIf="!element.error" (click)="watchContent(element)" mat-icon-button matTooltip="Watch content" i18n-matTooltip="Watch content"><mat-icon>smart_display</mat-icon></button>
|
<div *ngIf="minimizeButtons">
|
||||||
<button *ngIf="element.error" (click)="showError(element)" mat-icon-button matTooltip="Show error" i18n-matTooltip="Show error"><mat-icon>warning</mat-icon></button>
|
<button [matMenuTriggerFor]="download_actions" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||||
<button (click)="restartDownload(element.uid)" mat-icon-button matTooltip="Restart" i18n-matTooltip="Restart"><mat-icon>restart_alt</mat-icon></button>
|
<mat-menu #download_actions="matMenu">
|
||||||
</ng-container>
|
<ng-container *ngFor="let downloadAction of downloadActions">
|
||||||
<button *ngIf="element.finished || element.paused" (click)="clearDownload(element.uid)" mat-icon-button matTooltip="Clear" i18n-matTooltip="Clear"><mat-icon>delete</mat-icon></button>
|
<button *ngIf="downloadAction.show(element)" (click)="downloadAction.action(element)" [disabled]="downloadAction.loading && downloadAction.loading(element)" mat-menu-item>
|
||||||
|
<mat-icon>{{downloadAction.icon}}</mat-icon>
|
||||||
|
<span>{{downloadAction.tooltip}}</span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -80,9 +86,9 @@
|
|||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!uids" class="downloads-action-button-div">
|
<div *ngIf="!uids" class="downloads-action-button-div">
|
||||||
<button [disabled]="!running_download_exists" mat-stroked-button (click)="pauseAllDownloads()"><ng-container i18n="Pause all downloads">Pause all downloads</ng-container></button>
|
<button class="downloads-action-button" [disabled]="!running_download_exists" mat-stroked-button (click)="pauseAllDownloads()"><ng-container i18n="Pause all downloads">Pause all downloads</ng-container></button>
|
||||||
<button style="margin-left: 10px;" [disabled]="!paused_download_exists" mat-stroked-button (click)="resumeAllDownloads()"><ng-container i18n="Resume all downloads">Resume all downloads</ng-container></button>
|
<button class="downloads-action-button" [disabled]="!paused_download_exists" mat-stroked-button (click)="resumeAllDownloads()"><ng-container i18n="Resume all downloads">Resume all downloads</ng-container></button>
|
||||||
<button color="warn" style="margin-left: 10px;" mat-stroked-button (click)="clearDownloadsByType()"><ng-container i18n="Clear downloads">Clear downloads</ng-container></button>
|
<button class="downloads-action-button" color="warn" mat-stroked-button (click)="clearDownloadsByType()"><ng-container i18n="Clear downloads">Clear downloads</ng-container></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,23 @@ mat-header-cell, mat-cell {
|
|||||||
|
|
||||||
.icon-button-spinner {
|
.icon-button-spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 7px;
|
top: -13px;
|
||||||
left: 6px;
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-span {
|
||||||
|
position: relative;;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloads-action-button-div {
|
.downloads-action-button-div {
|
||||||
margin-top: 10px;
|
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.downloads-action-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.rounded-top {
|
.rounded-top {
|
||||||
border-radius: 16px 16px 0px 0px !important;
|
border-radius: 16px 16px 0px 0px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, OnDestroy, ViewChild, Input, EventEmitter } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ViewChild, Input, EventEmitter, HostListener } from '@angular/core';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { trigger, transition, animateChild, stagger, query, style, animate } from '@angular/animations';
|
import { trigger, transition, animateChild, stagger, query, style, animate } from '@angular/animations';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -13,31 +13,7 @@ import { Download } from 'api-types';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-downloads',
|
selector: 'app-downloads',
|
||||||
templateUrl: './downloads.component.html',
|
templateUrl: './downloads.component.html',
|
||||||
styleUrls: ['./downloads.component.scss'],
|
styleUrls: ['./downloads.component.scss']
|
||||||
animations: [
|
|
||||||
// nice stagger effect when showing existing elements
|
|
||||||
trigger('list', [
|
|
||||||
transition(':enter', [
|
|
||||||
// child animation selector + stagger
|
|
||||||
query('@items',
|
|
||||||
stagger(100, animateChild()), { optional: true }
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
trigger('items', [
|
|
||||||
// cubic-bezier for a tiny bouncing feel
|
|
||||||
transition(':enter', [
|
|
||||||
style({ transform: 'scale(0.5)', opacity: 0 }),
|
|
||||||
animate('500ms cubic-bezier(.8,-0.6,0.2,1.5)',
|
|
||||||
style({ transform: 'scale(1)', opacity: 1 }))
|
|
||||||
]),
|
|
||||||
transition(':leave', [
|
|
||||||
style({ transform: 'scale(1)', opacity: 1, height: '*' }),
|
|
||||||
animate('1s cubic-bezier(.8,-0.6,0.2,1.5)',
|
|
||||||
style({ transform: 'scale(0.5)', opacity: 0, height: '0px', margin: '0px' }))
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class DownloadsComponent implements OnInit, OnDestroy {
|
export class DownloadsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@@ -62,13 +38,79 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
3: $localize`Complete`
|
3: $localize`Complete`
|
||||||
}
|
}
|
||||||
|
|
||||||
displayedColumns: string[] = ['timestamp_start', 'title', 'step_index', 'sub_name', 'percent_complete', 'actions'];
|
actionsFlex = 2;
|
||||||
|
minimizeButtons = false;
|
||||||
|
displayedColumnsBig: string[] = ['timestamp_start', 'title', 'sub_name', 'percent_complete', 'actions'];
|
||||||
|
displayedColumnsSmall: string[] = ['title', 'percent_complete', 'actions'];
|
||||||
|
displayedColumns: string[] = this.displayedColumnsBig;
|
||||||
dataSource = null; // new MatTableDataSource<Download>();
|
dataSource = null; // new MatTableDataSource<Download>();
|
||||||
|
|
||||||
|
// The purpose of this is to reduce code reuse for displaying these actions as icons or in a menu
|
||||||
|
downloadActions: DownloadAction[] = [
|
||||||
|
{
|
||||||
|
tooltip: $localize`Watch content`,
|
||||||
|
action: (download: Download) => this.watchContent(download),
|
||||||
|
show: (download: Download) => download.finished && !download.error,
|
||||||
|
icon: 'smart_display'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Show error`,
|
||||||
|
action: (download: Download) => this.showError(download),
|
||||||
|
show: (download: Download) => download.finished && !!download.error,
|
||||||
|
icon: 'warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Restart`,
|
||||||
|
action: (download: Download) => this.restartDownload(download),
|
||||||
|
show: (download: Download) => download.finished,
|
||||||
|
icon: 'restart_alt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Pause`,
|
||||||
|
action: (download: Download) => this.pauseDownload(download),
|
||||||
|
show: (download: Download) => !download.finished && (!download.paused || !download.finished_step),
|
||||||
|
icon: 'pause',
|
||||||
|
loading: (download: Download) => download.paused && !download.finished_step
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Resume`,
|
||||||
|
action: (download: Download) => this.resumeDownload(download),
|
||||||
|
show: (download: Download) => !download.finished && download.paused && download.finished_step,
|
||||||
|
icon: 'play_arrow'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Resume`,
|
||||||
|
action: (download: Download) => this.resumeDownload(download),
|
||||||
|
show: (download: Download) => !download.finished && download.paused && download.finished_step,
|
||||||
|
icon: 'play_arrow'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Cancel`,
|
||||||
|
action: (download: Download) => this.cancelDownload(download),
|
||||||
|
show: (download: Download) => false && !download.finished && !download.paused, // TODO: add possibility to cancel download
|
||||||
|
icon: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Clear`,
|
||||||
|
action: (download: Download) => this.clearDownload(download),
|
||||||
|
show: (download: Download) => download.finished || download.paused,
|
||||||
|
icon: 'delete'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
downloads_retrieved = false;
|
downloads_retrieved = false;
|
||||||
|
|
||||||
|
innerWidth: number;
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
onResize(): void {
|
||||||
|
this.innerWidth = window.innerWidth;
|
||||||
|
this.recalculateColumns();
|
||||||
|
}
|
||||||
|
|
||||||
sort_downloads = (a: Download, b: Download): number => {
|
sort_downloads = (a: Download, b: Download): number => {
|
||||||
const result = b.timestamp_start - a.timestamp_start;
|
const result = b.timestamp_start - a.timestamp_start;
|
||||||
return result;
|
return result;
|
||||||
@@ -77,6 +119,10 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog, private clipboard: Clipboard) { }
|
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog, private clipboard: Clipboard) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Remove sub name as it's not necessary for one-off downloads
|
||||||
|
if (this.uids) this.displayedColumnsBig = this.displayedColumnsBig.filter(col => col !== 'sub_name');
|
||||||
|
this.innerWidth = window.innerWidth;
|
||||||
|
this.recalculateColumns();
|
||||||
if (this.postsService.initialized) {
|
if (this.postsService.initialized) {
|
||||||
this.getCurrentDownloadsRecurring();
|
this.getCurrentDownloadsRecurring();
|
||||||
} else {
|
} else {
|
||||||
@@ -164,8 +210,8 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pauseDownload(download_uid: string): void {
|
pauseDownload(download: Download): void {
|
||||||
this.postsService.pauseDownload(download_uid).subscribe(res => {
|
this.postsService.pauseDownload(download['uid']).subscribe(res => {
|
||||||
if (!res['success']) {
|
if (!res['success']) {
|
||||||
this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
|
this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
|
||||||
}
|
}
|
||||||
@@ -180,8 +226,8 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeDownload(download_uid: string): void {
|
resumeDownload(download: Download): void {
|
||||||
this.postsService.resumeDownload(download_uid).subscribe(res => {
|
this.postsService.resumeDownload(download['uid']).subscribe(res => {
|
||||||
if (!res['success']) {
|
if (!res['success']) {
|
||||||
this.postsService.openSnackBar($localize`Failed to resume download! See server logs for more info.`);
|
this.postsService.openSnackBar($localize`Failed to resume download! See server logs for more info.`);
|
||||||
}
|
}
|
||||||
@@ -196,8 +242,8 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
restartDownload(download_uid: string): void {
|
restartDownload(download: Download): void {
|
||||||
this.postsService.restartDownload(download_uid).subscribe(res => {
|
this.postsService.restartDownload(download['uid']).subscribe(res => {
|
||||||
if (!res['success']) {
|
if (!res['success']) {
|
||||||
this.postsService.openSnackBar($localize`Failed to restart download! See server logs for more info.`);
|
this.postsService.openSnackBar($localize`Failed to restart download! See server logs for more info.`);
|
||||||
} else {
|
} else {
|
||||||
@@ -208,16 +254,16 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelDownload(download_uid: string): void {
|
cancelDownload(download: Download): void {
|
||||||
this.postsService.cancelDownload(download_uid).subscribe(res => {
|
this.postsService.cancelDownload(download['uid']).subscribe(res => {
|
||||||
if (!res['success']) {
|
if (!res['success']) {
|
||||||
this.postsService.openSnackBar($localize`Failed to cancel download! See server logs for more info.`);
|
this.postsService.openSnackBar($localize`Failed to cancel download! See server logs for more info.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clearDownload(download_uid: string): void {
|
clearDownload(download: Download): void {
|
||||||
this.postsService.clearDownload(download_uid).subscribe(res => {
|
this.postsService.clearDownload(download['uid']).subscribe(res => {
|
||||||
if (!res['success']) {
|
if (!res['success']) {
|
||||||
this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
|
this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
|
||||||
}
|
}
|
||||||
@@ -257,6 +303,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showError(download: Download): void {
|
showError(download: Download): void {
|
||||||
|
console.log(download)
|
||||||
const copyToClipboardEmitter = new EventEmitter<boolean>();
|
const copyToClipboardEmitter = new EventEmitter<boolean>();
|
||||||
this.dialog.open(ConfirmDialogComponent, {
|
this.dialog.open(ConfirmDialogComponent, {
|
||||||
data: {
|
data: {
|
||||||
@@ -276,4 +323,22 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recalculateColumns() {
|
||||||
|
if (this.innerWidth < 650) this.displayedColumns = this.displayedColumnsSmall;
|
||||||
|
else this.displayedColumns = this.displayedColumnsBig;
|
||||||
|
|
||||||
|
this.actionsFlex = this.uids || this.innerWidth < 800 ? 1 : 2;
|
||||||
|
|
||||||
|
if (this.innerWidth < 800 && !this.uids || this.innerWidth < 1100 && this.uids) this.minimizeButtons = true;
|
||||||
|
else this.minimizeButtons = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadAction {
|
||||||
|
tooltip: string,
|
||||||
|
action: (download: Download) => void,
|
||||||
|
show: (download: Download) => boolean,
|
||||||
|
icon: string,
|
||||||
|
loading?: (download: Download) => boolean
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,32 @@
|
|||||||
<div class="card-radius mat-elevation-z2" *ngFor="let notification of notifications; let i = index;">
|
<cdk-virtual-scroll-viewport itemSize="50" class="viewport" minBufferPx="1200" maxBufferPx="1200">
|
||||||
<mat-card class="notification-card card-radius">
|
<div #notification_parent class="notification-card-parent card-radius mat-elevation-z2" *cdkVirtualFor="let notification of notifications; let i = index;">
|
||||||
<mat-card-header>
|
<mat-card class="notification-card card-radius">
|
||||||
<mat-card-subtitle>
|
<mat-card-header>
|
||||||
<div>
|
<mat-card-subtitle>
|
||||||
<span class="notification-timestamp">{{notification.timestamp * 1000 | date:'short'}}</span>
|
<div>
|
||||||
</div>
|
<span class="notification-timestamp">{{notification.timestamp * 1000 | date:'short'}}</span>
|
||||||
</mat-card-subtitle>
|
</div>
|
||||||
<mat-card-title>
|
</mat-card-subtitle>
|
||||||
<ng-container *ngIf="NOTIFICATION_PREFIX[notification.type]">
|
<mat-card-title>
|
||||||
{{NOTIFICATION_PREFIX[notification.type]}}
|
<ng-container *ngIf="NOTIFICATION_PREFIX[notification.type]">
|
||||||
|
{{NOTIFICATION_PREFIX[notification.type]}}
|
||||||
|
</ng-container>
|
||||||
|
</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<ng-container *ngIf="NOTIFICATION_SUFFIX_KEY[notification.type]">
|
||||||
|
<div style="word-break: break-word">
|
||||||
|
{{notification['data'][NOTIFICATION_SUFFIX_KEY[notification.type]]}}
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-card-title>
|
</mat-card-content>
|
||||||
</mat-card-header>
|
<mat-card-actions class="notification-actions" *ngIf="notification.actions?.length > 0">
|
||||||
<mat-card-content>
|
<button matTooltip="Remove" i18n-matTooltip="Remove" (click)="emitDeleteNotification(notification.uid)" mat-icon-button><mat-icon>close</mat-icon></button>
|
||||||
<ng-container *ngIf="NOTIFICATION_SUFFIX_KEY[notification.type]">
|
<span *ngFor="let action of notification.actions">
|
||||||
<div style="word-break: break-word">
|
<button [matTooltip]="NOTIFICATION_ACTION_TO_STRING[action]" (click)="emitNotificationAction(notification, action)" mat-icon-button><mat-icon>{{NOTIFICATION_ICON[action]}}</mat-icon></button>
|
||||||
{{notification['data'][NOTIFICATION_SUFFIX_KEY[notification.type]]}}
|
</span>
|
||||||
</div>
|
</mat-card-actions>
|
||||||
</ng-container>
|
<span *ngIf="!notification.read" class="dot"></span>
|
||||||
</mat-card-content>
|
</mat-card>
|
||||||
<mat-card-actions *ngIf="notification.actions?.length > 0">
|
</div>
|
||||||
<button matTooltip="Remove" i18n-matTooltip="Remove" (click)="emitDeleteNotification(notification.uid)" mat-icon-button><mat-icon>close</mat-icon></button>
|
</cdk-virtual-scroll-viewport>
|
||||||
<span *ngFor="let action of notification.actions">
|
|
||||||
<button [matTooltip]="NOTIFICATION_ACTION_TO_STRING[action]" (click)="emitNotificationAction(notification, action)" mat-icon-button><mat-icon>{{NOTIFICATION_ICON[action]}}</mat-icon></button>
|
|
||||||
</span>
|
|
||||||
</mat-card-actions>
|
|
||||||
<span *ngIf="!notification.read" class="dot"></span>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
@@ -13,12 +13,21 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-card-parent {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-card {
|
.notification-card {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-actions {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.card-radius {
|
.card-radius {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
height: 166px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
@@ -30,4 +39,8 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.viewport {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -4,7 +4,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notifications-list-parent {
|
.notifications-list-parent {
|
||||||
max-height: 70vh;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0px 10px 10px 10px;
|
padding: 0px 10px 10px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notifications-list {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<mat-chip-listbox [value]="selectedFilters" [multiple]="true" (change)="selectedFiltersChanged($event)">
|
<mat-chip-listbox [value]="selectedFilters" [multiple]="true" (change)="selectedFiltersChanged($event)">
|
||||||
<mat-chip-option *ngFor="let filter of notificationFilters | keyvalue: originalOrder" [value]="filter.key" [selected]="selectedFilters.includes(filter.key)" color="accent">{{filter.value.label}}</mat-chip-option>
|
<mat-chip-option *ngFor="let filter of notificationFilters | keyvalue: originalOrder" [value]="filter.key" [selected]="selectedFilters.includes(filter.key)" color="accent">{{filter.value.label}}</mat-chip-option>
|
||||||
</mat-chip-listbox>
|
</mat-chip-listbox>
|
||||||
<app-notifications-list (notificationAction)="notificationAction($event)" (deleteNotification)="deleteNotification($event)" [notifications]="filtered_notifications"></app-notifications-list>
|
<app-notifications-list class="notifications-list" [style.height]="list_height" (notificationAction)="notificationAction($event)" (deleteNotification)="deleteNotification($event)" [notifications]="filtered_notifications"></app-notifications-list>
|
||||||
</div>
|
</div>
|
||||||
<button style="margin: 10px 0px 2px 10px;" *ngIf="notifications?.length > 0" color="warn" (click)="deleteAllNotifications()" mat-stroked-button>Remove all</button>
|
<button style="margin: 10px 0px 2px 10px;" *ngIf="notifications?.length > 0" color="warn" (click)="deleteAllNotifications()" mat-stroked-button>Remove all</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export class NotificationsComponent implements OnInit {
|
|||||||
|
|
||||||
notifications: Notification[] = null;
|
notifications: Notification[] = null;
|
||||||
filtered_notifications: Notification[] = null;
|
filtered_notifications: Notification[] = null;
|
||||||
|
list_height = '65vh';
|
||||||
|
|
||||||
@Output() notificationCount = new EventEmitter<number>();
|
@Output() notificationCount = new EventEmitter<number>();
|
||||||
|
|
||||||
@@ -110,6 +111,8 @@ export class NotificationsComponent implements OnInit {
|
|||||||
|
|
||||||
filterNotifications(): void {
|
filterNotifications(): void {
|
||||||
this.filtered_notifications = this.notifications.filter(notification => this.selectedFilters.length === 0 || this.selectedFilters.includes(notification.type));
|
this.filtered_notifications = this.notifications.filter(notification => this.selectedFilters.length === 0 || this.selectedFilters.includes(notification.type));
|
||||||
|
// We need to do this to get the virtual scroll component to have an appropriate height
|
||||||
|
this.calculateListHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedFiltersChanged(event: MatChipListboxChange): void {
|
selectedFiltersChanged(event: MatChipListboxChange): void {
|
||||||
@@ -117,6 +120,12 @@ export class NotificationsComponent implements OnInit {
|
|||||||
this.filterNotifications();
|
this.filterNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calculateListHeight() {
|
||||||
|
const avgHeight = 166;
|
||||||
|
const calcHeight = this.filtered_notifications.length * avgHeight;
|
||||||
|
this.list_height = calcHeight > window.innerHeight*0.65 ? '65vh' : `${calcHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
originalOrder = (): number => {
|
originalOrder = (): number => {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -35,36 +35,6 @@
|
|||||||
<p>
|
<p>
|
||||||
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container> <a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a> <ng-container i18n="About bug suffix">to create an issue!</ng-container>
|
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container> <a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a> <ng-container i18n="About bug suffix">to create an issue!</ng-container>
|
||||||
</p>
|
</p>
|
||||||
<mat-divider></mat-divider>
|
|
||||||
<div style="margin-top: 10px;">
|
|
||||||
<h5>Personal settings:</h5>
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label i18n="Sidepanel mode">Sidepanel mode</mat-label>
|
|
||||||
<mat-select [(ngModel)]="sidepanel_mode" (selectionChange)="sidePanelModeChanged($event.value)">
|
|
||||||
<mat-option value="over">
|
|
||||||
Over
|
|
||||||
</mat-option>
|
|
||||||
<mat-option value="side">
|
|
||||||
Side
|
|
||||||
</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
<br/>
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label i18n="File card size">File card size</mat-label>
|
|
||||||
<mat-select [(ngModel)]="card_size" (selectionChange)="cardSizeOptionChanged($event.value)">
|
|
||||||
<mat-option value="large">
|
|
||||||
Large
|
|
||||||
</mat-option>
|
|
||||||
<mat-option value="medium">
|
|
||||||
Medium
|
|
||||||
</mat-option>
|
|
||||||
<mat-option value="small">
|
|
||||||
Small
|
|
||||||
</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ export class AboutDialogComponent implements OnInit {
|
|||||||
checking_for_updates = true;
|
checking_for_updates = true;
|
||||||
|
|
||||||
current_version_tag = CURRENT_VERSION;
|
current_version_tag = CURRENT_VERSION;
|
||||||
sidepanel_mode = this.postsService.sidepanel_mode;
|
|
||||||
card_size = this.postsService.card_size;
|
|
||||||
|
|
||||||
constructor(public postsService: PostsService) { }
|
constructor(public postsService: PostsService) { }
|
||||||
|
|
||||||
@@ -31,15 +29,4 @@ export class AboutDialogComponent implements OnInit {
|
|||||||
this.latestGithubRelease = res;
|
this.latestGithubRelease = res;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sidePanelModeChanged(new_mode) {
|
|
||||||
localStorage.setItem('sidepanel_mode', new_mode);
|
|
||||||
this.postsService.sidepanel_mode = new_mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
cardSizeOptionChanged(new_size) {
|
|
||||||
localStorage.setItem('card_size', new_size);
|
|
||||||
this.postsService.card_size = new_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -13,19 +13,52 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
</div>
|
</div>
|
||||||
|
<mat-divider style="margin-bottom: 20px"></mat-divider>
|
||||||
</div>
|
</div>
|
||||||
|
<mat-form-field color="accent">
|
||||||
<div *ngIf="!postsService.isLoggedIn || !postsService.user">
|
<mat-label><ng-container i18n="Language select label">Language</ng-container></mat-label>
|
||||||
<h5><mat-icon>warn</mat-icon><ng-container i18n="Not logged in notification">You are not logged in.</ng-container></h5>
|
<mat-select (selectionChange)="localeSelectChanged($event.value)" [(value)]="initialLocale">
|
||||||
<button (click)="loginClicked()" mat-raised-button color="primary"><ng-container i18n="Login">Login</ng-container></button>
|
<mat-option *ngFor="let locale of supported_locales" [value]="locale">
|
||||||
</div>
|
<ng-container *ngIf="all_locales[locale]">
|
||||||
|
{{all_locales[locale]['nativeName']}}
|
||||||
|
</ng-container>
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<br/>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label i18n="Sidepanel mode">Sidepanel mode</mat-label>
|
||||||
|
<mat-select [(ngModel)]="sidepanel_mode" (selectionChange)="sidePanelModeChanged($event.value)">
|
||||||
|
<mat-option i18n="Over" value="over">
|
||||||
|
Over
|
||||||
|
</mat-option>
|
||||||
|
<mat-option i18n="Side" value="side">
|
||||||
|
Side
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<br/>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label i18n="File card size">File card size</mat-label>
|
||||||
|
<mat-select [(ngModel)]="card_size" (selectionChange)="cardSizeOptionChanged($event.value)">
|
||||||
|
<mat-option i18n="Large" value="large">
|
||||||
|
Large
|
||||||
|
</mat-option>
|
||||||
|
<mat-option i18n="Medium" value="medium">
|
||||||
|
Medium
|
||||||
|
</mat-option>
|
||||||
|
<mat-option i18n="Small" value="small">
|
||||||
|
Small
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<div style="width: 100%">
|
<div style="width: 100%">
|
||||||
<div style="position: relative">
|
<div style="position: relative">
|
||||||
<button mat-stroked-button mat-dialog-close color="primary"><ng-container i18n="Close">Close</ng-container></button>
|
<button mat-stroked-button mat-dialog-close color="primary"><ng-container i18n="Close">Close</ng-container></button>
|
||||||
<button style="position: absolute; right: 0px;" (click)="logoutClicked()" mat-stroked-button color="warn"><ng-container i18n="Logout">Logout</ng-container></button>
|
<button *ngIf="postsService.isLoggedIn" style="position: absolute; right: 0px;" (click)="logoutClicked()" mat-stroked-button color="warn"><ng-container i18n="Logout">Logout</ng-container></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MatDialogRef } from '@angular/material/dialog';
|
import { MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { isoLangs } from './locales_list';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-profile-dialog',
|
selector: 'app-user-profile-dialog',
|
||||||
@@ -10,9 +11,24 @@ import { MatDialogRef } from '@angular/material/dialog';
|
|||||||
})
|
})
|
||||||
export class UserProfileDialogComponent implements OnInit {
|
export class UserProfileDialogComponent implements OnInit {
|
||||||
|
|
||||||
|
all_locales = isoLangs;
|
||||||
|
supported_locales = ['en', 'es', 'de', 'fr', 'nl', 'pt', 'it', 'ca', 'cs', 'nb', 'ru', 'zh', 'ko', 'id', 'en-GB'];
|
||||||
|
initialLocale = localStorage.getItem('locale');
|
||||||
|
sidepanel_mode = this.postsService.sidepanel_mode;
|
||||||
|
card_size = this.postsService.card_size;
|
||||||
|
|
||||||
constructor(public postsService: PostsService, private router: Router, public dialogRef: MatDialogRef<UserProfileDialogComponent>) { }
|
constructor(public postsService: PostsService, private router: Router, public dialogRef: MatDialogRef<UserProfileDialogComponent>) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.postsService.getSupportedLocales().subscribe(res => {
|
||||||
|
if (res && res['supported_locales']) {
|
||||||
|
this.supported_locales = ['en', 'en-GB']; // required
|
||||||
|
this.supported_locales = this.supported_locales.concat(res['supported_locales']);
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
console.error(`Failed to retrieve list of supported languages! You may need to run: 'node src/postbuild.mjs'. Error below:`);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loginClicked() {
|
loginClicked() {
|
||||||
@@ -25,4 +41,19 @@ export class UserProfileDialogComponent implements OnInit {
|
|||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localeSelectChanged(new_val: string): void {
|
||||||
|
localStorage.setItem('locale', new_val);
|
||||||
|
this.postsService.openSnackBar($localize`Language successfully changed! Reload to update the page.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
sidePanelModeChanged(new_mode) {
|
||||||
|
localStorage.setItem('sidepanel_mode', new_mode);
|
||||||
|
this.postsService.sidepanel_mode = new_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
cardSizeOptionChanged(new_size) {
|
||||||
|
localStorage.setItem('card_size', new_size);
|
||||||
|
this.postsService.card_size = new_size;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -205,7 +205,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: center;" *ngIf="downloads && downloads.length > 0 && !autoplay">
|
<div style="display: flex; justify-content: center;" *ngIf="downloads && downloads.length > 0 && !autoplay">
|
||||||
<app-downloads style="width: 80%; margin-bottom: 10px" [uids]="download_uids"></app-downloads>
|
<app-downloads style="width: 80%; min-width: 350px; margin-bottom: 10px" [uids]="download_uids"></app-downloads>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|||||||
@@ -37,11 +37,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
bottom: 1px;
|
bottom: -2px;
|
||||||
left: 2px;
|
left: 6px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.save-button {
|
.save-button {
|
||||||
right: 25px;
|
right: 25px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -85,13 +89,6 @@
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-div {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 12px;
|
|
||||||
top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-ad-button {
|
.skip-ad-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
|
|||||||
@@ -22,23 +22,25 @@
|
|||||||
</p>
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!db_file || !db_file['description']">
|
<ng-container *ngIf="!db_file || !db_file['description']">
|
||||||
<p style="text-align: center;">
|
<p i18n="No description" style="text-align: center;">
|
||||||
No description available.
|
No description available.
|
||||||
</p>
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<ng-container *ngIf="db_playlist">
|
<span class="buttons" *ngIf="db_playlist">
|
||||||
<button (click)="downloadContent()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
|
<button (click)="downloadContent()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon></button>
|
||||||
|
<mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner>
|
||||||
<button *ngIf="(!postsService.isLoggedIn || postsService.permissions.includes('sharing')) && !auto" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
<button *ngIf="(!postsService.isLoggedIn || postsService.permissions.includes('sharing')) && !auto" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
||||||
</ng-container>
|
</span>
|
||||||
<ng-container *ngIf="db_file">
|
<span class="buttons" *ngIf="db_file">
|
||||||
<button (click)="downloadFile()" [disabled]="downloading" mat-icon-button><mat-icon>cloud_download</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
|
<button (click)="downloadFile()" [disabled]="downloading" mat-icon-button><mat-icon>cloud_download</mat-icon></button>
|
||||||
|
<mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner>
|
||||||
<button *ngIf="!postsService.isLoggedIn || postsService.permissions.includes('sharing')" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
<button *ngIf="!postsService.isLoggedIn || postsService.permissions.includes('sharing')" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
||||||
</ng-container>
|
</span>
|
||||||
<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,19 +53,11 @@
|
|||||||
|
|
||||||
<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>
|
||||||
</mat-drawer>
|
</mat-drawer>
|
||||||
|
|
||||||
<!-- <div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
|
|
||||||
<div class="spinner-div">
|
|
||||||
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
|
|
||||||
</div>
|
|
||||||
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button><ng-container i18n="Playlist save changes button">Save changes</ng-container> <mat-icon>update</mat-icon></button>
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
</mat-drawer-container>
|
</mat-drawer-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
url = null;
|
url = null;
|
||||||
name = null;
|
name = null;
|
||||||
|
|
||||||
innerWidth: number;
|
|
||||||
|
|
||||||
downloading = false;
|
downloading = false;
|
||||||
|
|
||||||
save_volume_timer = null;
|
save_volume_timer = null;
|
||||||
@@ -70,14 +68,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
@ViewChild('twitchchat') twitchChat: TwitchChatComponent;
|
@ViewChild('twitchchat') twitchChat: TwitchChatComponent;
|
||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
|
||||||
onResize(): void {
|
|
||||||
this.innerWidth = window.innerWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.innerWidth = window.innerWidth;
|
|
||||||
|
|
||||||
this.playlist_id = this.route.snapshot.paramMap.get('playlist_id');
|
this.playlist_id = this.route.snapshot.paramMap.get('playlist_id');
|
||||||
this.uid = this.route.snapshot.paramMap.get('uid');
|
this.uid = this.route.snapshot.paramMap.get('uid');
|
||||||
this.sub_id = this.route.snapshot.paramMap.get('sub_id');
|
this.sub_id = this.route.snapshot.paramMap.get('sub_id');
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -115,7 +114,7 @@ import {
|
|||||||
Subscription,
|
Subscription,
|
||||||
RestartDownloadResponse
|
RestartDownloadResponse
|
||||||
} from '../api-types';
|
} from '../api-types';
|
||||||
import { isoLangs } from './settings/locales_list';
|
import { isoLangs } from './dialogs/user-profile-dialog/locales_list';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { MatDrawerMode } from '@angular/material/sidenav';
|
import { MatDrawerMode } from '@angular/material/sidenav';
|
||||||
|
|
||||||
@@ -131,7 +130,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 +185,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
|
||||||
@@ -742,7 +734,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 +788,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({
|
||||||
|
|||||||
@@ -78,23 +78,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 mt-3">
|
|
||||||
<mat-form-field color="accent">
|
|
||||||
<mat-label><ng-container i18n="Language select label">Language</ng-container></mat-label>
|
|
||||||
<mat-select (selectionChange)="localeSelectChanged($event.value)" [(value)]="initialLocale">
|
|
||||||
<mat-option *ngFor="let locale of supported_locales" [value]="locale">
|
|
||||||
<ng-container *ngIf="all_locales[locale]">
|
|
||||||
{{all_locales[locale]['nativeName']}}
|
|
||||||
</ng-container>
|
|
||||||
</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<!-- Downloader -->
|
<!-- Downloader -->
|
||||||
@@ -269,25 +252,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 +368,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Component, OnInit, EventEmitter } from '@angular/core';
|
import { Component, OnInit, EventEmitter } from '@angular/core';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { isoLangs } from './locales_list';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import {DomSanitizer} from '@angular/platform-browser';
|
import {DomSanitizer} from '@angular/platform-browser';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
@@ -22,10 +21,6 @@ import { GenerateRssUrlComponent } from 'app/dialogs/generate-rss-url/generate-r
|
|||||||
styleUrls: ['./settings.component.scss']
|
styleUrls: ['./settings.component.scss']
|
||||||
})
|
})
|
||||||
export class SettingsComponent implements OnInit {
|
export class SettingsComponent implements OnInit {
|
||||||
all_locales = isoLangs;
|
|
||||||
supported_locales = ['en', 'es', 'de', 'fr', 'nl', 'pt', 'it', 'ca', 'cs', 'nb', 'ru', 'zh', 'ko', 'id', 'en-GB'];
|
|
||||||
initialLocale = localStorage.getItem('locale');
|
|
||||||
|
|
||||||
initial_config = null;
|
initial_config = null;
|
||||||
new_config = null
|
new_config = null
|
||||||
loading_config = false;
|
loading_config = false;
|
||||||
@@ -41,7 +36,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);
|
||||||
@@ -83,16 +78,6 @@ export class SettingsComponent implements OnInit {
|
|||||||
|
|
||||||
const tab = this.route.snapshot.paramMap.get('tab');
|
const tab = this.route.snapshot.paramMap.get('tab');
|
||||||
this.tabIndex = tab && this.TAB_TO_INDEX[tab] ? this.TAB_TO_INDEX[tab] : 0;
|
this.tabIndex = tab && this.TAB_TO_INDEX[tab] ? this.TAB_TO_INDEX[tab] : 0;
|
||||||
|
|
||||||
this.postsService.getSupportedLocales().subscribe(res => {
|
|
||||||
if (res && res['supported_locales']) {
|
|
||||||
this.supported_locales = ['en', 'en-GB']; // required
|
|
||||||
this.supported_locales = this.supported_locales.concat(res['supported_locales']);
|
|
||||||
}
|
|
||||||
}, err => {
|
|
||||||
console.error(`Failed to retrieve list of supported languages! You may need to run: 'node src/postbuild.mjs'. Error below:`);
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig(): void {
|
getConfig(): void {
|
||||||
@@ -207,11 +192,6 @@ export class SettingsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
localeSelectChanged(new_val: string): void {
|
|
||||||
localStorage.setItem('locale', new_val);
|
|
||||||
this.postsService.openSnackBar($localize`Language successfully changed! Reload to update the page.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
generateBookmarklet(): void {
|
generateBookmarklet(): void {
|
||||||
this.bookmarksite('YTDL-Material', this.generated_bookmarklet_code);
|
this.bookmarksite('YTDL-Material', this.generated_bookmarklet_code);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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