mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-28 23:50:59 +03:00
Compare commits
85 Commits
dependabot
...
angular-17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2baa30d9a | ||
|
|
a74bca05cc | ||
|
|
7d3458ea41 | ||
|
|
6a7c1c9d0b | ||
|
|
e2e3dd280a | ||
|
|
25bf7a6fdd | ||
|
|
c56987ddd5 | ||
|
|
72399b09e4 | ||
|
|
4258b82040 | ||
|
|
7ac6a50b41 | ||
|
|
fb92975b73 | ||
|
|
b4cf1e39b9 | ||
|
|
026f24a327 | ||
|
|
1bf348f481 | ||
|
|
eb8cd3fd06 | ||
|
|
f96ffab530 | ||
|
|
dcb53691e3 | ||
|
|
2cf21541bb | ||
|
|
13e46397e9 | ||
|
|
7f079c56d0 | ||
|
|
e082919cd0 | ||
|
|
a89378b99f | ||
|
|
4dc899439e | ||
|
|
9b38c56528 | ||
|
|
0644b194d0 | ||
|
|
344d959c05 | ||
|
|
3912655912 | ||
|
|
cdf82abf3f | ||
|
|
84464db0e0 | ||
|
|
4bf03bfd1a | ||
|
|
75cbe4d5d0 | ||
|
|
9556f9c94f | ||
|
|
4a97fa4ef5 | ||
|
|
2c155b74a9 | ||
|
|
25e4c114e8 | ||
|
|
6152df3486 | ||
|
|
7cf5d86fc3 | ||
|
|
f57e0ab187 | ||
|
|
517c9e169d | ||
|
|
69d8751484 | ||
|
|
c3c8f50a92 | ||
|
|
caadf4f9d2 | ||
|
|
d10401cead | ||
|
|
d02d100001 | ||
|
|
6b59446a37 | ||
|
|
4fd25e1e49 | ||
|
|
d30c338189 | ||
|
|
509e996107 | ||
|
|
240e87b453 | ||
|
|
eaefcc5b96 | ||
|
|
85577ac528 | ||
|
|
41050ce923 | ||
|
|
55bc5339f5 | ||
|
|
0e33b2db2b | ||
|
|
1456c25978 | ||
|
|
67c38039b0 | ||
|
|
8f246d905f | ||
|
|
91c2fdc701 | ||
|
|
2c97403027 | ||
|
|
3151200d33 | ||
|
|
c5ed835b09 | ||
|
|
8a588cf858 | ||
|
|
2396c86486 | ||
|
|
2cc2428db2 | ||
|
|
80e83ba817 | ||
|
|
0565cf24a6 | ||
|
|
353c35cd8d | ||
|
|
169a057c37 | ||
|
|
ab6d0f199e | ||
|
|
ae48a4c195 | ||
|
|
241473b99d | ||
|
|
ba98548662 | ||
|
|
72419d7be9 | ||
|
|
50079d2ab7 | ||
|
|
ee21f79fff | ||
|
|
097a3509c1 | ||
|
|
cc0fa03aca | ||
|
|
477cba93cd | ||
|
|
eda3dfcac7 | ||
|
|
188876e383 | ||
|
|
2c70e1367d | ||
|
|
7012524c61 | ||
|
|
cc6dfbf928 | ||
|
|
6ebda81225 | ||
|
|
a50476ac58 |
39
.devcontainer/devcontainer.json
Normal file
39
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
|
||||||
|
{
|
||||||
|
"name": "Node.js",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/javascript-node:0-18-bullseye",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-contrib/features/jshint:2": {},
|
||||||
|
"ghcr.io/devcontainers-contrib/features/angular-cli:2": {},
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
"forwardPorts": [4200, 17442],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
"postCreateCommand": "npm install && cd backend && npm install",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
"customizations": {
|
||||||
|
// Configure properties specific to VS Code.
|
||||||
|
"vscode": {
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.python",
|
||||||
|
"Angular.ng-template",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"waderyan.gitblame",
|
||||||
|
"42Crunch.vscode-openapi",
|
||||||
|
"christian-kohler.npm-intellisense",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
"hbenl.vscode-mocha-test-adapter",
|
||||||
|
"DavidAnson.vscode-markdownlint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
||||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -13,11 +13,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: setup node
|
- name: setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '18'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
if: contains(github.ref, '/tags/v')
|
if: contains(github.ref, '/tags/v')
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: create release
|
- name: create release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
|
|||||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
|
|||||||
4
.github/workflows/docker-pr.yml
vendored
4
.github/workflows/docker-pr.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Set hash
|
- name: Set hash
|
||||||
id: vars
|
id: vars
|
||||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: setup multi-arch docker build
|
- name: setup multi-arch docker build
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: build & push images
|
- name: build & push images
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
10
.github/workflows/docker-release.yml
vendored
10
.github/workflows/docker-release.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set hash
|
- name: Set hash
|
||||||
id: vars
|
id: vars
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate Docker image metadata
|
- name: Generate Docker image metadata
|
||||||
id: docker-meta
|
id: docker-meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
|
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
|
||||||
@@ -63,20 +63,20 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: build & push images
|
- name: build & push images
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
10
.github/workflows/docker.yml
vendored
10
.github/workflows/docker.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set hash
|
- name: Set hash
|
||||||
id: vars
|
id: vars
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate Docker image metadata
|
- name: Generate Docker image metadata
|
||||||
id: docker-meta
|
id: docker-meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
# Defaults:
|
# Defaults:
|
||||||
# DOCKERHUB_USERNAME : tzahi12345
|
# DOCKERHUB_USERNAME : tzahi12345
|
||||||
# DOCKERHUB_REPO : youtubedl-material
|
# DOCKERHUB_REPO : youtubedl-material
|
||||||
@@ -63,20 +63,20 @@ 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@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: build & push images
|
- name: build & push images
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
6
.github/workflows/mocha.yml
vendored
6
.github/workflows/mocha.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node:
|
node:
|
||||||
- 16
|
- 18
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '${{ matrix.node }}'
|
node-version: '${{ matrix.node }}'
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: 'Cache node_modules'
|
- name: 'Cache node_modules'
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- uses: FedericoCarboni/setup-ffmpeg@v2
|
- uses: FedericoCarboni/setup-ffmpeg@v2
|
||||||
id: setup-ffmpeg
|
id: setup-ffmpeg
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install --dev
|
||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
- name: Run All Node.js Tests
|
- name: Run All Node.js Tests
|
||||||
run: npm run test
|
run: npm run test
|
||||||
|
|||||||
50
Dockerfile
50
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# Fetching our utils
|
# Fetching our utils
|
||||||
FROM ubuntu:22.04 AS utils
|
FROM ubuntu:23.04 AS utils
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
# Use script due local build compability
|
# Use script due local build compability
|
||||||
COPY docker-utils/*.sh .
|
COPY docker-utils/*.sh .
|
||||||
@@ -8,41 +8,39 @@ RUN sh ./ffmpeg-fetch.sh
|
|||||||
RUN sh ./fetch-twitchdownloader.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 18.19.0
|
||||||
# Go to 20.04
|
FROM ubuntu:23.04 AS base
|
||||||
FROM ubuntu:22.04 AS base
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ENV UID=1000
|
ENV UID=1001
|
||||||
ENV GID=1000
|
ENV GID=1001
|
||||||
ENV USER=youtube
|
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
|
ENV npm_config_cache=/app/.npm
|
||||||
## https://deb.nodesource.com/node_16.x/pool/main/n/nodejs/nodejs_16.14.2-deb-1nodesource1_amd64.deb
|
|
||||||
|
# Use NVM to get specific node version
|
||||||
|
ENV NODE_VERSION=18.19.0
|
||||||
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 libicu70 && \
|
apt install -y --no-install-recommends curl ca-certificates tzdata libatomic1 && \
|
||||||
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;
|
|
||||||
|
|
||||||
|
RUN mkdir /usr/local/nvm
|
||||||
|
ENV PATH="/usr/local/nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"
|
||||||
|
ENV NVM_DIR=/usr/local/nvm
|
||||||
|
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
|
||||||
|
RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
|
||||||
|
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
|
||||||
|
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
|
||||||
|
|
||||||
|
RUN npm install -g npm
|
||||||
|
|
||||||
# Build frontend
|
# Build frontend
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
FROM --platform=${BUILDPLATFORM} node:16 as frontend
|
FROM --platform=${BUILDPLATFORM} node:18 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/" ]
|
||||||
@@ -59,6 +57,8 @@ FROM base as backend
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY [ "backend/","/app/" ]
|
COPY [ "backend/","/app/" ]
|
||||||
RUN npm config set strict-ssl false && \
|
RUN npm config set strict-ssl false && \
|
||||||
|
npm config set registry https://registry.npm.taobao.org && \
|
||||||
|
npm config set fetch-retry-maxtimeout 60000 && \
|
||||||
npm install --prod && \
|
npm install --prod && \
|
||||||
ls -al
|
ls -al
|
||||||
|
|
||||||
@@ -75,10 +75,10 @@ RUN npm config set strict-ssl false && \
|
|||||||
|
|
||||||
# Final image
|
# Final image
|
||||||
FROM base
|
FROM base
|
||||||
RUN npm install -g pm2 && \
|
RUN apt update && \
|
||||||
apt update && \
|
curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash && \
|
||||||
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 && \
|
pip install pycryptodomex --break-system-packages && \
|
||||||
apt remove -y --purge build-essential && \
|
apt remove -y --purge build-essential && \
|
||||||
apt autoremove -y --purge && \
|
apt autoremove -y --purge && \
|
||||||
apt clean && \
|
apt clean && \
|
||||||
|
|||||||
@@ -293,6 +293,48 @@ paths:
|
|||||||
$ref: '#/components/schemas/UnsubscribeResponse'
|
$ref: '#/components/schemas/UnsubscribeResponse'
|
||||||
security:
|
security:
|
||||||
- Auth query parameter: []
|
- Auth query parameter: []
|
||||||
|
/api/checkSubscription:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- subscriptions
|
||||||
|
summary: Run a check for videos for a subscription
|
||||||
|
description: Runs a subscription check
|
||||||
|
operationId: post-api-checksubscription
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CheckSubscriptionRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
|
/api/cancelCheckSubscription:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- subscriptions
|
||||||
|
summary: Cancels check for videos for a subscription
|
||||||
|
description: Cancels subscription check
|
||||||
|
operationId: post-api-checksubscription
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CheckSubscriptionRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
/api/deleteSubscriptionFile:
|
/api/deleteSubscriptionFile:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -1981,11 +2023,11 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
UnsubscribeRequest:
|
UnsubscribeRequest:
|
||||||
required:
|
required:
|
||||||
- sub
|
- sub_id
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
sub:
|
sub_id:
|
||||||
$ref: '#/components/schemas/SubscriptionRequestData'
|
type: string
|
||||||
deleteMode:
|
deleteMode:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Defaults to false
|
description: Defaults to false
|
||||||
@@ -1998,6 +2040,13 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
error:
|
error:
|
||||||
type: string
|
type: string
|
||||||
|
CheckSubscriptionRequest:
|
||||||
|
required:
|
||||||
|
- sub_id
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
sub_id:
|
||||||
|
type: string
|
||||||
DeleteAllFilesResponse:
|
DeleteAllFilesResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -2683,6 +2732,8 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
paused:
|
paused:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
cancelled:
|
||||||
|
type: boolean
|
||||||
finished_step:
|
finished_step:
|
||||||
type: boolean
|
type: boolean
|
||||||
url:
|
url:
|
||||||
@@ -2841,6 +2892,8 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
isPlaylist:
|
isPlaylist:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
child_process:
|
||||||
|
type: object
|
||||||
archive:
|
archive:
|
||||||
type: string
|
type: string
|
||||||
timerange:
|
timerange:
|
||||||
@@ -2849,6 +2902,10 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
custom_output:
|
custom_output:
|
||||||
type: string
|
type: string
|
||||||
|
downloading:
|
||||||
|
type: boolean
|
||||||
|
paused:
|
||||||
|
type: boolean
|
||||||
videos:
|
videos:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
||||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
||||||
|
|
||||||
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 15](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 17](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||||
|
|
||||||
Now with [Docker](#Docker) support!
|
Now with [Docker](#Docker) support!
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker
|
|||||||
|
|
||||||
Required dependencies:
|
Required dependencies:
|
||||||
|
|
||||||
* Node.js 16
|
* Node.js 18
|
||||||
* Python
|
* Python
|
||||||
|
|
||||||
Optional dependencies:
|
Optional dependencies:
|
||||||
@@ -42,7 +42,7 @@ Optional dependencies:
|
|||||||
<summary>Debian/Ubuntu</summary>
|
<summary>Debian/Ubuntu</summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
curl -fsSL https://deb.nodesource.com/setup_18.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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ 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 -
|
curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -
|
||||||
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
19
angular.json
19
angular.json
@@ -66,6 +66,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"codespaces": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.codespaces.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"localize": ["es"]
|
"localize": ["es"]
|
||||||
}
|
}
|
||||||
@@ -75,21 +83,24 @@
|
|||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "youtube-dl-material:build"
|
"buildTarget": "youtube-dl-material:build"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "youtube-dl-material:build:production"
|
"buildTarget": "youtube-dl-material:build:production"
|
||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"browserTarget": "youtube-dl-material:build:es"
|
"buildTarget": "youtube-dl-material:build:es"
|
||||||
|
},
|
||||||
|
"codespaces": {
|
||||||
|
"buildTarget": "youtube-dl-material:build:codespaces"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "youtube-dl-material:build"
|
"buildTarget": "youtube-dl-material:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve-electron": {
|
"serve-electron": {
|
||||||
|
|||||||
184
backend/app.js
184
backend/app.js
@@ -1,4 +1,4 @@
|
|||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
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');
|
||||||
@@ -20,11 +20,6 @@ const ps = require('ps-node');
|
|||||||
const Feed = require('feed').Feed;
|
const Feed = require('feed').Feed;
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
|
|
||||||
// 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"})
|
|
||||||
|
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const config_api = require('./config.js');
|
const config_api = require('./config.js');
|
||||||
const downloader_api = require('./downloader');
|
const downloader_api = require('./downloader');
|
||||||
@@ -35,6 +30,7 @@ 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');
|
const files_api = require('./files');
|
||||||
|
const notifications_api = require('./notifications');
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
@@ -536,15 +532,9 @@ async function loadConfig() {
|
|||||||
// set downloading to false
|
// set downloading to false
|
||||||
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
||||||
subscriptions.forEach(async sub => subscriptions_api.writeSubscriptionMetadata(sub));
|
subscriptions.forEach(async sub => subscriptions_api.writeSubscriptionMetadata(sub));
|
||||||
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false});
|
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false, child_process: null});
|
||||||
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
||||||
const watchSubscriptionsInterval = function() {
|
subscriptions_api.watchSubscriptionsInterval();
|
||||||
watchSubscriptions();
|
|
||||||
const subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
|
||||||
setTimeout(watchSubscriptionsInterval, subscriptionsCheckInterval*1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
watchSubscriptionsInterval();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the server here
|
// start the server here
|
||||||
@@ -574,63 +564,8 @@ function loadConfigValues() {
|
|||||||
utils.updateLoggerLevel(logger_level);
|
utils.updateLoggerLevel(logger_level);
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateSubcriptionRetrievalDelay(subscriptions_amount) {
|
|
||||||
// frequency is once every 5 mins by default
|
|
||||||
const subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
|
||||||
let interval_in_ms = subscriptionsCheckInterval * 1000;
|
|
||||||
const subinterval_in_ms = interval_in_ms/subscriptions_amount;
|
|
||||||
return subinterval_in_ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function watchSubscriptions() {
|
|
||||||
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
|
||||||
|
|
||||||
if (!subscriptions) return;
|
|
||||||
|
|
||||||
// auto pause deprecated streamingOnly mode
|
|
||||||
const streaming_only_subs = subscriptions.filter(sub => sub.streamingOnly);
|
|
||||||
subscriptions_api.updateSubscriptionPropertyMultiple(streaming_only_subs, {paused: true});
|
|
||||||
|
|
||||||
const valid_subscriptions = subscriptions.filter(sub => !sub.paused && !sub.streamingOnly);
|
|
||||||
|
|
||||||
let subscriptions_amount = valid_subscriptions.length;
|
|
||||||
let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount);
|
|
||||||
|
|
||||||
let current_delay = 0;
|
|
||||||
|
|
||||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
|
||||||
for (let i = 0; i < valid_subscriptions.length; i++) {
|
|
||||||
let sub = valid_subscriptions[i];
|
|
||||||
|
|
||||||
// don't check the sub if the last check for the same subscription has not completed
|
|
||||||
if (subscription_timeouts[sub.id]) {
|
|
||||||
logger.verbose(`Subscription: skipped checking ${sub.name} as the last check for ${sub.name} has not completed.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sub.name) {
|
|
||||||
logger.verbose(`Subscription: skipped check for subscription with uid ${sub.id} as name has not been retrieved yet.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.verbose('Watching ' + sub.name + ' with delay interval of ' + delay_interval);
|
|
||||||
setTimeout(async () => {
|
|
||||||
const multiUserModeChanged = config_api.getConfigItem('ytdl_multi_user_mode') !== multiUserMode;
|
|
||||||
if (multiUserModeChanged) {
|
|
||||||
logger.verbose(`Skipping subscription ${sub.name} due to multi-user mode change.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await subscriptions_api.getVideosForSub(sub, sub.user_uid);
|
|
||||||
subscription_timeouts[sub.id] = false;
|
|
||||||
}, current_delay);
|
|
||||||
subscription_timeouts[sub.id] = true;
|
|
||||||
current_delay += delay_interval;
|
|
||||||
const subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
|
||||||
if (current_delay >= subscriptionsCheckInterval * 1000) current_delay = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOrigin() {
|
function getOrigin() {
|
||||||
|
if (process.env.CODESPACES) return `https://${process.env.CODESPACE_NAME}-4200.${process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
|
||||||
return url_domain.origin;
|
return url_domain.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,38 +590,11 @@ function generateEnvVarConfigItem(key) {
|
|||||||
return {key: key, value: process['env'][key]};
|
return {key: key, value: process['env'][key]};
|
||||||
}
|
}
|
||||||
|
|
||||||
// currently only works for single urls
|
|
||||||
async function getUrlInfos(url) {
|
|
||||||
let startDate = Date.now();
|
|
||||||
let result = [];
|
|
||||||
return new Promise(resolve => {
|
|
||||||
youtubedl.exec(url, ['--dump-json'], {maxBuffer: Infinity}, (err, output) => {
|
|
||||||
let new_date = Date.now();
|
|
||||||
let difference = (new_date - startDate)/1000;
|
|
||||||
logger.debug(`URL info retrieval delay: ${difference} seconds.`);
|
|
||||||
if (err) {
|
|
||||||
logger.error(`Error during retrieving formats for ${url}: ${err}`);
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
let try_putput = null;
|
|
||||||
try {
|
|
||||||
try_putput = JSON.parse(output);
|
|
||||||
result = try_putput;
|
|
||||||
} catch(e) {
|
|
||||||
logger.error(`Failed to retrieve available formats for url: ${url}`);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// youtube-dl functions
|
// youtube-dl functions
|
||||||
|
|
||||||
async function startYoutubeDL() {
|
async function startYoutubeDL() {
|
||||||
// auto update youtube-dl
|
// auto update youtube-dl
|
||||||
youtubedl_api.verifyBinaryExistsLinux();
|
await youtubedl_api.checkForYoutubeDLUpdate();
|
||||||
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
|
|
||||||
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(function(req, res, next) {
|
app.use(function(req, res, next) {
|
||||||
@@ -706,7 +614,7 @@ app.use(function(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
||||||
next();
|
next();
|
||||||
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss')) {
|
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss') || req.path.includes('/api/telegramRequest')) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
||||||
@@ -1212,10 +1120,10 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
app.post('/api/unsubscribe', optionalJwt, async (req, res) => {
|
app.post('/api/unsubscribe', optionalJwt, async (req, res) => {
|
||||||
let deleteMode = req.body.deleteMode
|
let deleteMode = req.body.deleteMode
|
||||||
let sub = req.body.sub;
|
let sub_id = req.body.sub_id;
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
let result_obj = subscriptions_api.unsubscribe(sub, deleteMode, user_uid);
|
let result_obj = subscriptions_api.unsubscribe(sub_id, deleteMode, user_uid);
|
||||||
if (result_obj.success) {
|
if (result_obj.success) {
|
||||||
res.send({
|
res.send({
|
||||||
success: result_obj.success
|
success: result_obj.success
|
||||||
@@ -1285,21 +1193,49 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/downloadVideosForSubscription', optionalJwt, async (req, res) => {
|
app.post('/api/downloadVideosForSubscription', optionalJwt, async (req, res) => {
|
||||||
let subID = req.body.subID;
|
const subID = req.body.subID;
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
|
||||||
|
|
||||||
let sub = subscriptions_api.getSubscription(subID, user_uid);
|
const sub = subscriptions_api.getSubscription(subID);
|
||||||
subscriptions_api.getVideosForSub(sub, user_uid);
|
subscriptions_api.getVideosForSub(sub.id);
|
||||||
res.send({
|
res.send({
|
||||||
success: true
|
success: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/updateSubscription', optionalJwt, async (req, res) => {
|
app.post('/api/updateSubscription', optionalJwt, async (req, res) => {
|
||||||
let updated_sub = req.body.subscription;
|
const updated_sub = req.body.subscription;
|
||||||
|
|
||||||
|
const success = subscriptions_api.updateSubscription(updated_sub);
|
||||||
|
res.send({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/checkSubscription', optionalJwt, async (req, res) => {
|
||||||
|
let sub_id = req.body.sub_id;
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
let success = subscriptions_api.updateSubscription(updated_sub, user_uid);
|
const success = subscriptions_api.getVideosForSub(sub_id, user_uid);
|
||||||
|
res.send({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/cancelCheckSubscription', optionalJwt, async (req, res) => {
|
||||||
|
let sub_id = req.body.sub_id;
|
||||||
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
const success = subscriptions_api.cancelCheckSubscription(sub_id, user_uid);
|
||||||
|
res.send({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/cancelSubscriptionCheck', optionalJwt, async (req, res) => {
|
||||||
|
let sub_id = req.body.sub_id;
|
||||||
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
const success = subscriptions_api.getVideosForSub(sub_id, user_uid);
|
||||||
res.send({
|
res.send({
|
||||||
success: success
|
success: success
|
||||||
});
|
});
|
||||||
@@ -1643,6 +1579,7 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
|
|||||||
}
|
}
|
||||||
if (!fs.existsSync(file_path)) {
|
if (!fs.existsSync(file_path)) {
|
||||||
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
|
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const stat = fs.statSync(file_path);
|
const stat = fs.statSync(file_path);
|
||||||
const fileSize = stat.size;
|
const fileSize = stat.size;
|
||||||
@@ -1777,6 +1714,10 @@ app.post('/api/cancelDownload', optionalJwt, async (req, res) => {
|
|||||||
app.post('/api/getTasks', optionalJwt, async (req, res) => {
|
app.post('/api/getTasks', optionalJwt, async (req, res) => {
|
||||||
const tasks = await db_api.getRecords('tasks');
|
const tasks = await db_api.getRecords('tasks');
|
||||||
for (let task of tasks) {
|
for (let task of tasks) {
|
||||||
|
if (!tasks_api.TASKS[task['key']]) {
|
||||||
|
logger.verbose(`Task ${task['key']} does not exist!`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task['key']]['job'].nextInvocation().getTime();
|
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task['key']]['job'].nextInvocation().getTime();
|
||||||
}
|
}
|
||||||
res.send({tasks: tasks});
|
res.send({tasks: tasks});
|
||||||
@@ -1919,11 +1860,11 @@ app.post('/api/clearAllLogs', optionalJwt, async function(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/getFileFormats', optionalJwt, async (req, res) => {
|
app.post('/api/getFileFormats', optionalJwt, async (req, res) => {
|
||||||
let url = req.body.url;
|
const url = req.body.url;
|
||||||
let result = await getUrlInfos(url);
|
const result = await downloader_api.getVideoInfoByURL(url);
|
||||||
res.send({
|
res.send({
|
||||||
result: result,
|
result: result && result.length === 1 ? result[0] : null,
|
||||||
success: !!result
|
success: result && result.length === 0
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2085,6 +2026,25 @@ app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => {
|
|||||||
res.send({success: success});
|
res.send({success: success});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/telegramRequest', async (req, res) => {
|
||||||
|
if (!req.body.message && !req.body.message.text) {
|
||||||
|
logger.error('Invalid Telegram request received!');
|
||||||
|
res.sendStatus(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = req.body.message.text;
|
||||||
|
const regex_exp = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi;
|
||||||
|
const url_regex = new RegExp(regex_exp);
|
||||||
|
if (text.match(url_regex)) {
|
||||||
|
downloader_api.createDownload(text, 'video', {}, req.query.user_uid ? req.query.user_uid : null);
|
||||||
|
res.sendStatus(200);
|
||||||
|
} else {
|
||||||
|
logger.error('Invalid Telegram request received! Make sure you only send a valid URL.');
|
||||||
|
notifications_api.sendTelegramNotification({title: 'Invalid Telegram Request', body: 'Make sure you only send a valid URL.', url: text});
|
||||||
|
res.sendStatus(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// rss feed
|
// rss feed
|
||||||
|
|
||||||
app.get('/api/rss', async function (req, res) {
|
app.get('/api/rss', async function (req, res) {
|
||||||
@@ -2152,6 +2112,8 @@ app.use(function(req, res, next) {
|
|||||||
|
|
||||||
let index_path = path.join(__dirname, 'public', 'index.html');
|
let index_path = path.join(__dirname, 'public', 'index.html');
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
fs.createReadStream(index_path).pipe(res);
|
fs.createReadStream(index_path).pipe(res);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"use_telegram_API": false,
|
"use_telegram_API": false,
|
||||||
"telegram_bot_token": "",
|
"telegram_bot_token": "",
|
||||||
"telegram_chat_id": "",
|
"telegram_chat_id": "",
|
||||||
|
"telegram_webhook_proxy": "",
|
||||||
"webhook_URL": "",
|
"webhook_URL": "",
|
||||||
"discord_webhook_URL": "",
|
"discord_webhook_URL": "",
|
||||||
"slack_webhook_URL": ""
|
"slack_webhook_URL": ""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const logger = require('../logger');
|
|||||||
const db_api = require('../db');
|
const db_api = require('../db');
|
||||||
|
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|||||||
@@ -32,10 +32,8 @@ async function categorize(file_jsons) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < file_jsons.length; i++) {
|
for (const file_json of file_jsons) {
|
||||||
const file_json = file_jsons[i];
|
for (const category of categories) {
|
||||||
for (let j = 0; j < categories.length; j++) {
|
|
||||||
const category = categories[j];
|
|
||||||
const rules = category['rules'];
|
const rules = category['rules'];
|
||||||
|
|
||||||
// if rules for current category apply, then that is the selected category
|
// if rules for current category apply, then that is the selected category
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const { BehaviorSubject } = require('rxjs');
|
||||||
|
|
||||||
|
exports.CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
||||||
|
exports.descriptors = {}; // to get rid of file locks when needed, TODO: move to youtube-dl.js
|
||||||
|
|
||||||
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
|
||||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
|
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
|
||||||
|
exports.config_updated = new BehaviorSubject();
|
||||||
|
|
||||||
function initialize() {
|
exports.initialize = () => {
|
||||||
ensureConfigFileExists();
|
ensureConfigFileExists();
|
||||||
ensureConfigItemsExist();
|
ensureConfigItemsExist();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureConfigItemsExist() {
|
function ensureConfigItemsExist() {
|
||||||
const config_keys = Object.keys(CONFIG_ITEMS);
|
const config_keys = Object.keys(exports.CONFIG_ITEMS);
|
||||||
for (let i = 0; i < config_keys.length; i++) {
|
for (let i = 0; i < config_keys.length; i++) {
|
||||||
const config_key = config_keys[i];
|
const config_key = config_keys[i];
|
||||||
getConfigItem(config_key);
|
exports.getConfigItem(config_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,17 +61,17 @@ function getElementNameInConfig(path) {
|
|||||||
/**
|
/**
|
||||||
* Check if config exists. If not, write default config to config path
|
* Check if config exists. If not, write default config to config path
|
||||||
*/
|
*/
|
||||||
function configExistsCheck() {
|
exports.configExistsCheck = () => {
|
||||||
let exists = fs.existsSync(configPath);
|
let exists = fs.existsSync(configPath);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
setConfigFile(DEFAULT_CONFIG);
|
exports.setConfigFile(DEFAULT_CONFIG);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Gets config file and returns as a json
|
* Gets config file and returns as a json
|
||||||
*/
|
*/
|
||||||
function getConfigFile() {
|
exports.getConfigFile = () => {
|
||||||
try {
|
try {
|
||||||
let raw_data = fs.readFileSync(configPath);
|
let raw_data = fs.readFileSync(configPath);
|
||||||
let parsed_data = JSON.parse(raw_data);
|
let parsed_data = JSON.parse(raw_data);
|
||||||
@@ -78,35 +82,40 @@ function getConfigFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConfigFile(config) {
|
exports.setConfigFile = (config) => {
|
||||||
try {
|
try {
|
||||||
|
const old_config = exports.getConfigFile();
|
||||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||||
|
const changes = exports.findChangedConfigItems(old_config, config);
|
||||||
|
if (changes.length > 0) {
|
||||||
|
for (const change of changes) exports.config_updated.next(change);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfigItem(key) {
|
exports.getConfigItem = (key) => {
|
||||||
let config_json = getConfigFile();
|
let config_json = exports.getConfigFile();
|
||||||
if (!CONFIG_ITEMS[key]) {
|
if (!exports.CONFIG_ITEMS[key]) {
|
||||||
logger.error(`Config item with key '${key}' is not recognized.`);
|
logger.error(`Config item with key '${key}' is not recognized.`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let path = CONFIG_ITEMS[key]['path'];
|
let path = exports.CONFIG_ITEMS[key]['path'];
|
||||||
const val = Object.byString(config_json, path);
|
const val = Object.byString(config_json, path);
|
||||||
if (val === undefined && Object.byString(DEFAULT_CONFIG, path) !== undefined) {
|
if (val === undefined && Object.byString(DEFAULT_CONFIG, path) !== undefined) {
|
||||||
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
|
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
|
||||||
setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
|
exports.setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
|
||||||
return Object.byString(DEFAULT_CONFIG, path);
|
return Object.byString(DEFAULT_CONFIG, path);
|
||||||
}
|
}
|
||||||
return Object.byString(config_json, path);
|
return Object.byString(config_json, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConfigItem(key, value) {
|
exports.setConfigItem = (key, value) => {
|
||||||
let success = false;
|
let success = false;
|
||||||
let config_json = getConfigFile();
|
let config_json = exports.getConfigFile();
|
||||||
let path = CONFIG_ITEMS[key]['path'];
|
let path = exports.CONFIG_ITEMS[key]['path'];
|
||||||
let element_name = getElementNameInConfig(path);
|
let element_name = getElementNameInConfig(path);
|
||||||
let parent_path = getParentPath(path);
|
let parent_path = getParentPath(path);
|
||||||
let parent_object = Object.byString(config_json, parent_path);
|
let parent_object = Object.byString(config_json, parent_path);
|
||||||
@@ -118,20 +127,18 @@ function setConfigItem(key, value) {
|
|||||||
parent_parent_object[parent_parent_single_key] = {};
|
parent_parent_object[parent_parent_single_key] = {};
|
||||||
parent_object = Object.byString(config_json, parent_path);
|
parent_object = Object.byString(config_json, parent_path);
|
||||||
}
|
}
|
||||||
|
if (value === 'false') value = false;
|
||||||
if (value === 'false' || value === 'true') {
|
if (value === 'true') value = true;
|
||||||
parent_object[element_name] = (value === 'true');
|
|
||||||
} else {
|
|
||||||
parent_object[element_name] = value;
|
parent_object[element_name] = value;
|
||||||
}
|
|
||||||
success = setConfigFile(config_json);
|
success = exports.setConfigFile(config_json);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConfigItems(items) {
|
exports.setConfigItems = (items) => {
|
||||||
let success = false;
|
let success = false;
|
||||||
let config_json = getConfigFile();
|
let config_json = exports.getConfigFile();
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
let key = items[i].key;
|
let key = items[i].key;
|
||||||
let value = items[i].value;
|
let value = items[i].value;
|
||||||
@@ -141,7 +148,7 @@ function setConfigItems(items) {
|
|||||||
value = (value === 'true');
|
value = (value === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
let item_path = CONFIG_ITEMS[key]['path'];
|
let item_path = exports.CONFIG_ITEMS[key]['path'];
|
||||||
let item_parent_path = getParentPath(item_path);
|
let item_parent_path = getParentPath(item_path);
|
||||||
let item_element_name = getElementNameInConfig(item_path);
|
let item_element_name = getElementNameInConfig(item_path);
|
||||||
|
|
||||||
@@ -149,28 +156,41 @@ function setConfigItems(items) {
|
|||||||
item_parent_object[item_element_name] = value;
|
item_parent_object[item_element_name] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
success = setConfigFile(config_json);
|
success = exports.setConfigFile(config_json);
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
function globalArgsRequiresSafeDownload() {
|
exports.globalArgsRequiresSafeDownload = () => {
|
||||||
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
|
const globalArgs = exports.getConfigItem('ytdl_custom_args').split(',,');
|
||||||
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
|
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
|
||||||
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
|
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
|
||||||
return failedArgs && failedArgs.length > 0;
|
return failedArgs && failedArgs.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
exports.findChangedConfigItems = (old_config, new_config, path = '', changedConfigItems = [], depth = 0) => {
|
||||||
getConfigItem: getConfigItem,
|
if (typeof old_config === 'object' && typeof new_config === 'object' && depth < 3) {
|
||||||
setConfigItem: setConfigItem,
|
for (const key in old_config) {
|
||||||
setConfigItems: setConfigItems,
|
if (Object.prototype.hasOwnProperty.call(new_config, key)) {
|
||||||
getConfigFile: getConfigFile,
|
exports.findChangedConfigItems(old_config[key], new_config[key], `${path}${path ? '.' : ''}${key}`, changedConfigItems, depth + 1);
|
||||||
setConfigFile: setConfigFile,
|
}
|
||||||
configExistsCheck: configExistsCheck,
|
}
|
||||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
} else {
|
||||||
initialize: initialize,
|
if (JSON.stringify(old_config) !== JSON.stringify(new_config)) {
|
||||||
descriptors: {},
|
const key = getConfigItemKeyByPath(path);
|
||||||
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
|
changedConfigItems.push({
|
||||||
|
key: key ? key : path.split('.')[path.split('.').length - 1], // return key in CONFIG_ITEMS or the object key
|
||||||
|
old_value: JSON.parse(JSON.stringify(old_config)),
|
||||||
|
new_value: JSON.parse(JSON.stringify(new_config))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changedConfigItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigItemKeyByPath(path) {
|
||||||
|
const found_item = Object.values(exports.CONFIG_ITEMS).find(item => item.path === path);
|
||||||
|
if (found_item) return found_item['key'];
|
||||||
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
@@ -219,6 +239,7 @@ const DEFAULT_CONFIG = {
|
|||||||
"use_telegram_API": false,
|
"use_telegram_API": false,
|
||||||
"telegram_bot_token": "",
|
"telegram_bot_token": "",
|
||||||
"telegram_chat_id": "",
|
"telegram_chat_id": "",
|
||||||
|
"telegram_webhook_proxy": "",
|
||||||
"webhook_URL": "",
|
"webhook_URL": "",
|
||||||
"discord_webhook_URL": "",
|
"discord_webhook_URL": "",
|
||||||
"slack_webhook_URL": "",
|
"slack_webhook_URL": "",
|
||||||
|
|||||||
@@ -154,6 +154,10 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_telegram_chat_id',
|
'key': 'ytdl_telegram_chat_id',
|
||||||
'path': 'YoutubeDLMaterial.API.telegram_chat_id'
|
'path': 'YoutubeDLMaterial.API.telegram_chat_id'
|
||||||
},
|
},
|
||||||
|
'ytdl_telegram_webhook_proxy': {
|
||||||
|
'key': 'ytdl_telegram_webhook_proxy',
|
||||||
|
'path': 'YoutubeDLMaterial.API.telegram_webhook_proxy'
|
||||||
|
},
|
||||||
'ytdl_webhook_url': {
|
'ytdl_webhook_url': {
|
||||||
'key': 'ytdl_webhook_url',
|
'key': 'ytdl_webhook_url',
|
||||||
'path': 'YoutubeDLMaterial.API.webhook_URL'
|
'path': 'YoutubeDLMaterial.API.webhook_URL'
|
||||||
@@ -269,7 +273,8 @@ exports.AVAILABLE_PERMISSIONS = [
|
|||||||
'tasks_manager'
|
'tasks_manager'
|
||||||
];
|
];
|
||||||
|
|
||||||
exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details'
|
exports.DETAILS_BIN_PATH = 'appdata/youtube-dl.json'
|
||||||
|
exports.OUTDATED_YOUTUBEDL_VERSION = "2020.00.00";
|
||||||
|
|
||||||
// args that have a value after it (e.g. -o <output> or -f <format>)
|
// args that have a value after it (e.g. -o <output> or -f <format>)
|
||||||
const YTDL_ARGS_WITH_VALUES = [
|
const YTDL_ARGS_WITH_VALUES = [
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { MongoClient } = require("mongodb");
|
const { MongoClient } = require("mongodb");
|
||||||
const { uuid } = require('uuidv4');
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const NodeID3 = require('node-id3')
|
const NodeID3 = require('node-id3')
|
||||||
const Mutex = require('async-mutex').Mutex;
|
const Mutex = require('async-mutex').Mutex;
|
||||||
|
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const youtubedl_api = require('./youtube-dl');
|
const youtubedl_api = require('./youtube-dl');
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
@@ -21,6 +19,8 @@ const archive_api = require('./archive');
|
|||||||
const mutex = new Mutex();
|
const mutex = new Mutex();
|
||||||
let should_check_downloads = true;
|
let should_check_downloads = true;
|
||||||
|
|
||||||
|
const download_to_child_process = {};
|
||||||
|
|
||||||
if (db_api.database_initialized) {
|
if (db_api.database_initialized) {
|
||||||
exports.setupDownloads();
|
exports.setupDownloads();
|
||||||
} else {
|
} else {
|
||||||
@@ -84,8 +84,11 @@ exports.pauseDownload = async (download_uid) => {
|
|||||||
} else if (download['finished']) {
|
} else if (download['finished']) {
|
||||||
logger.info(`Download ${download_uid} could not be paused before completing.`);
|
logger.info(`Download ${download_uid} could not be paused before completing.`);
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.info(`Pausing download ${download_uid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
killActiveDownload(download);
|
||||||
return await db_api.updateRecord('download_queue', {uid: download_uid}, {paused: true, running: false});
|
return await db_api.updateRecord('download_queue', {uid: download_uid}, {paused: true, running: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,16 +123,23 @@ exports.cancelDownload = async (download_uid) => {
|
|||||||
} else if (download['finished']) {
|
} else if (download['finished']) {
|
||||||
logger.info(`Download ${download_uid} could not be cancelled before completing.`);
|
logger.info(`Download ${download_uid} could not be cancelled before completing.`);
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.info(`Cancelling download ${download_uid}`);
|
||||||
}
|
}
|
||||||
return await db_api.updateRecord('download_queue', {uid: download_uid}, {cancelled: true, running: false});
|
|
||||||
|
killActiveDownload(download);
|
||||||
|
await handleDownloadError(download_uid, 'Cancelled', 'cancelled');
|
||||||
|
return await db_api.updateRecord('download_queue', {uid: download_uid}, {cancelled: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.clearDownload = async (download_uid) => {
|
exports.clearDownload = async (download_uid) => {
|
||||||
return await db_api.removeRecord('download_queue', {uid: download_uid});
|
return await db_api.removeRecord('download_queue', {uid: download_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDownloadError(download, error_message, error_type = null) {
|
async function handleDownloadError(download_uid, error_message, error_type = null) {
|
||||||
if (!download || !download['uid']) return;
|
if (!download_uid) return;
|
||||||
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
if (!download || download['error']) return;
|
||||||
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_message, 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});
|
||||||
}
|
}
|
||||||
@@ -180,14 +190,14 @@ async function checkDownloads() {
|
|||||||
if (waiting_download['sub_id']) {
|
if (waiting_download['sub_id']) {
|
||||||
const sub_missing = !(await db_api.getRecord('subscriptions', {id: waiting_download['sub_id']}));
|
const sub_missing = !(await db_api.getRecord('subscriptions', {id: waiting_download['sub_id']}));
|
||||||
if (sub_missing) {
|
if (sub_missing) {
|
||||||
handleDownloadError(waiting_download, `Download failed as subscription with id '${waiting_download['sub_id']}' is missing!`, 'sub_id_missing');
|
handleDownloadError(waiting_download['uid'], `Download failed as subscription with id '${waiting_download['sub_id']}' is missing!`, 'sub_id_missing');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// move to next step
|
// move to next step
|
||||||
running_downloads_count++;
|
running_downloads_count++;
|
||||||
if (waiting_download['step_index'] === 0) {
|
if (waiting_download['step_index'] === 0) {
|
||||||
collectInfo(waiting_download['uid']);
|
exports.collectInfo(waiting_download['uid']);
|
||||||
} else if (waiting_download['step_index'] === 1) {
|
} else if (waiting_download['step_index'] === 1) {
|
||||||
exports.downloadQueuedFile(waiting_download['uid']);
|
exports.downloadQueuedFile(waiting_download['uid']);
|
||||||
}
|
}
|
||||||
@@ -195,7 +205,15 @@ async function checkDownloads() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectInfo(download_uid) {
|
function killActiveDownload(download) {
|
||||||
|
const child_process = download_to_child_process[download['uid']];
|
||||||
|
if (download['step_index'] === 2 && child_process) {
|
||||||
|
youtubedl_api.killYoutubeDLProcess(child_process);
|
||||||
|
delete download_to_child_process[download['uid']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.collectInfo = async (download_uid) => {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
if (download['paused']) {
|
if (download['paused']) {
|
||||||
return;
|
return;
|
||||||
@@ -218,21 +236,21 @@ async function collectInfo(download_uid) {
|
|||||||
// get video info prior to download
|
// get video info prior to download
|
||||||
let info = download['prefetched_info'] ? download['prefetched_info'] : await exports.getVideoInfoByURL(url, args, download_uid);
|
let info = download['prefetched_info'] ? download['prefetched_info'] : await exports.getVideoInfoByURL(url, args, download_uid);
|
||||||
|
|
||||||
if (!info) {
|
if (!info || info.length === 0) {
|
||||||
// info failed, error presumably already recorded
|
// info failed, error presumably already recorded
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// in subscriptions we don't care if archive mode is enabled, but we already removed archived videos from subs by this point
|
// 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 && info.length === 1) {
|
||||||
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info['id'], type, download['user_uid'], download['sub_id']);
|
const info_obj = info[0];
|
||||||
|
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info_obj['id'], type, download['user_uid'], download['sub_id']);
|
||||||
if (exists_in_archive) {
|
if (exists_in_archive) {
|
||||||
const error = `File '${info['title']}' already exists in archive! Disable the archive or override to continue downloading.`;
|
const error = `File '${info_obj['title']}' already exists in archive! Disable the archive or override to continue downloading.`;
|
||||||
logger.warn(error);
|
logger.warn(error);
|
||||||
if (download_uid) {
|
if (download_uid) {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
await handleDownloadError(download_uid, error, 'exists_in_archive');
|
||||||
await handleDownloadError(download, error, 'exists_in_archive');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +259,7 @@ async function collectInfo(download_uid) {
|
|||||||
let category = null;
|
let category = null;
|
||||||
|
|
||||||
// check if it fits into a category. If so, then get info again using new args
|
// check if it fits into a category. If so, then get info again using new args
|
||||||
if (info.length === 0 || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
if (info.length === 1 || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
||||||
|
|
||||||
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
||||||
if (category && category['custom_output']) {
|
if (category && category['custom_output']) {
|
||||||
@@ -262,20 +280,20 @@ async function collectInfo(download_uid) {
|
|||||||
// store info in download for future use
|
// store info in download for future use
|
||||||
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
|
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
|
||||||
|
|
||||||
const playlist_title = info.length > 0 ? info[0]['playlist_title'] || info[0]['playlist'] : null;
|
const title = info.length > 1 ? info[0]['playlist_title'] || info[0]['playlist'] : info[0]['title'];
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
|
||||||
finished_step: true,
|
finished_step: true,
|
||||||
running: false,
|
running: false,
|
||||||
options: options,
|
options: options,
|
||||||
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: title,
|
||||||
category: stripped_category,
|
category: stripped_category,
|
||||||
prefetched_info: null
|
prefetched_info: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.downloadQueuedFile = async(download_uid, downloadMethod = youtubedl.exec) => {
|
exports.downloadQueuedFile = async(download_uid, customDownloadHandler = null) => {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
if (download['paused']) {
|
if (download['paused']) {
|
||||||
return;
|
return;
|
||||||
@@ -305,21 +323,25 @@ exports.downloadQueuedFile = async(download_uid, downloadMethod = youtubedl.exec
|
|||||||
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
|
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
|
||||||
const file_objs = [];
|
const file_objs = [];
|
||||||
// download file
|
// download file
|
||||||
const {parsed_output, err} = await youtubedl_api.runYoutubeDL(url, args, downloadMethod);
|
let {child_process, callback} = await youtubedl_api.runYoutubeDL(url, args, customDownloadHandler);
|
||||||
|
if (child_process) download_to_child_process[download['uid']] = child_process;
|
||||||
|
const {parsed_output, err} = await callback;
|
||||||
clearInterval(download_checker);
|
clearInterval(download_checker);
|
||||||
let end_time = Date.now();
|
let end_time = Date.now();
|
||||||
let difference = (end_time - start_time)/1000;
|
let difference = (end_time - start_time)/1000;
|
||||||
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
||||||
if (!parsed_output) {
|
if (!parsed_output) {
|
||||||
logger.error(err.stderr);
|
const errored_download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
await handleDownloadError(download, err.stderr, 'unknown_error');
|
if (errored_download && errored_download['paused']) return;
|
||||||
|
logger.error(err.toString());
|
||||||
|
await handleDownloadError(download_uid, err.toString(), 'unknown_error');
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
} else if (parsed_output) {
|
} else if (parsed_output) {
|
||||||
if (parsed_output.length === 0 || parsed_output[0].length === 0) {
|
if (parsed_output.length === 0 || parsed_output[0].length === 0) {
|
||||||
// ERROR!
|
// ERROR!
|
||||||
const error_message = `No output received for video download, check if it exists in your archive.`;
|
const error_message = `No output received for video download, check if it exists in your archive.`;
|
||||||
await handleDownloadError(download, error_message, 'no_output');
|
await handleDownloadError(download_uid, error_message, 'no_output');
|
||||||
logger.warn(error_message);
|
logger.warn(error_message);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
@@ -385,14 +407,13 @@ exports.downloadQueuedFile = async(download_uid, downloadMethod = youtubedl.exec
|
|||||||
|
|
||||||
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(', ');
|
container = await files_api.createPlaylist(download['title'], 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 {
|
||||||
const error_message = 'Downloaded file failed to result in metadata object.';
|
const error_message = 'Downloaded file failed to result in metadata object.';
|
||||||
logger.error(error_message);
|
logger.error(error_message);
|
||||||
await handleDownloadError(download, error_message, 'no_metadata');
|
await handleDownloadError(download_uid, error_message, 'no_metadata');
|
||||||
}
|
}
|
||||||
|
|
||||||
const file_uids = file_objs.map(file_obj => file_obj.uid);
|
const file_uids = file_objs.map(file_obj => file_obj.uid);
|
||||||
@@ -405,7 +426,7 @@ exports.downloadQueuedFile = async(download_uid, downloadMethod = youtubedl.exec
|
|||||||
// helper functions
|
// helper functions
|
||||||
|
|
||||||
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
|
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
|
||||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
|
||||||
if (!simulated && (default_downloader === 'youtube-dl' || default_downloader === 'youtube-dlc')) {
|
if (!simulated && (default_downloader === 'youtube-dl' || default_downloader === 'youtube-dlc')) {
|
||||||
logger.warn('It is recommended you use yt-dlp! To prevent failed downloads, change the downloader in your settings menu to yt-dlp and restart your instance.')
|
logger.warn('It is recommended you use yt-dlp! To prevent failed downloads, change the downloader in your settings menu to yt-dlp and restart your instance.')
|
||||||
@@ -500,6 +521,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
downloadConfig.push('--write-thumbnail');
|
downloadConfig.push('--write-thumbnail');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadConfig.push('-i');
|
||||||
|
|
||||||
if (globalArgs && globalArgs !== '') {
|
if (globalArgs && globalArgs !== '') {
|
||||||
// adds global args
|
// adds global args
|
||||||
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
|
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
|
||||||
@@ -536,7 +559,6 @@ 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 => {
|
|
||||||
// remove bad args
|
// remove bad args
|
||||||
const temp_args = utils.filterArgs(args, ['--no-simulate']);
|
const temp_args = utils.filterArgs(args, ['--no-simulate']);
|
||||||
const new_args = [...temp_args];
|
const new_args = [...temp_args];
|
||||||
@@ -548,22 +570,19 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
|||||||
|
|
||||||
new_args.push('--dump-json');
|
new_args.push('--dump-json');
|
||||||
|
|
||||||
youtubedl.exec(url, new_args, {maxBuffer: Infinity}, async (err, output) => {
|
let {callback} = await youtubedl_api.runYoutubeDL(url, new_args);
|
||||||
const parsed_output = utils.parseOutputJSON(output, err);
|
const {parsed_output, err} = await callback;
|
||||||
if (parsed_output) {
|
if (!parsed_output || parsed_output.length === 0) {
|
||||||
resolve(parsed_output);
|
let error_message = `Error while retrieving info on video with URL ${url}`;
|
||||||
} else {
|
if (err.stderr) error_message += ` with the following message: \n${err.stderr}`;
|
||||||
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
|
|
||||||
if (err.stderr) error_message += `\n\n${err.stderr}`;
|
|
||||||
logger.error(error_message);
|
logger.error(error_message);
|
||||||
if (download_uid) {
|
if (download_uid) {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
await handleDownloadError(download_uid, error_message, 'info_retrieve_failed');
|
||||||
await handleDownloadError(download, error_message, 'info_retrieve_failed');
|
|
||||||
}
|
}
|
||||||
resolve(null);
|
return null;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
return parsed_output;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterArgs(args, isAudio) {
|
function filterArgs(args, isAudio) {
|
||||||
@@ -582,6 +601,7 @@ async function checkDownloadPercent(download_uid) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
if (!download) return;
|
||||||
const files_to_check_for_progress = download['files_to_check_for_progress'];
|
const files_to_check_for_progress = download['files_to_check_for_progress'];
|
||||||
const resulting_file_size = download['expected_file_size'];
|
const resulting_file_size = download['expected_file_size'];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ const logger = require('./logger');
|
|||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const consts = require('./consts');
|
const consts = require('./consts');
|
||||||
|
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
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 TelegramBotAPI = require('node-telegram-bot-api');
|
||||||
|
let telegram_bot = null;
|
||||||
const REST = require('@discordjs/rest').REST;
|
const REST = require('@discordjs/rest').REST;
|
||||||
const API = require('@discordjs/core').API;
|
const API = require('@discordjs/core').API;
|
||||||
const EmbedBuilder = require('@discordjs/builders').EmbedBuilder;
|
const EmbedBuilder = require('@discordjs/builders').EmbedBuilder;
|
||||||
@@ -56,7 +57,7 @@ exports.sendNotification = async (notification) => {
|
|||||||
sendGotifyNotification(data);
|
sendGotifyNotification(data);
|
||||||
}
|
}
|
||||||
if (config_api.getConfigItem('ytdl_use_telegram_API') && config_api.getConfigItem('ytdl_telegram_bot_token') && config_api.getConfigItem('ytdl_telegram_chat_id')) {
|
if (config_api.getConfigItem('ytdl_use_telegram_API') && config_api.getConfigItem('ytdl_telegram_bot_token') && config_api.getConfigItem('ytdl_telegram_chat_id')) {
|
||||||
sendTelegramNotification(data);
|
exports.sendTelegramNotification(data);
|
||||||
}
|
}
|
||||||
if (config_api.getConfigItem('ytdl_webhook_url')) {
|
if (config_api.getConfigItem('ytdl_webhook_url')) {
|
||||||
sendGenericNotification(data);
|
sendGenericNotification(data);
|
||||||
@@ -113,6 +114,8 @@ function notificationEnabled(type) {
|
|||||||
return config_api.getConfigItem('ytdl_enable_notifications') && (config_api.getConfigItem('ytdl_enable_all_notifications') || config_api.getConfigItem('ytdl_allowed_notification_types').includes(type));
|
return config_api.getConfigItem('ytdl_enable_notifications') && (config_api.getConfigItem('ytdl_enable_all_notifications') || config_api.getConfigItem('ytdl_allowed_notification_types').includes(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ntfy
|
||||||
|
|
||||||
function sendNtfyNotification({body, title, type, url, thumbnail}) {
|
function sendNtfyNotification({body, title, type, url, thumbnail}) {
|
||||||
logger.verbose('Sending notification to ntfy');
|
logger.verbose('Sending notification to ntfy');
|
||||||
fetch(config_api.getConfigItem('ytdl_ntfy_topic_url'), {
|
fetch(config_api.getConfigItem('ytdl_ntfy_topic_url'), {
|
||||||
@@ -127,6 +130,8 @@ function sendNtfyNotification({body, title, type, url, thumbnail}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gotify
|
||||||
|
|
||||||
async function sendGotifyNotification({body, title, type, url, thumbnail}) {
|
async function sendGotifyNotification({body, title, type, url, thumbnail}) {
|
||||||
logger.verbose('Sending notification to gotify');
|
logger.verbose('Sending notification to gotify');
|
||||||
await gotify({
|
await gotify({
|
||||||
@@ -145,14 +150,49 @@ async function sendGotifyNotification({body, title, type, url, thumbnail}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendTelegramNotification({body, title, type, url, thumbnail}) {
|
// Telegram
|
||||||
logger.verbose('Sending notification to Telegram');
|
|
||||||
|
setupTelegramBot();
|
||||||
|
config_api.config_updated.subscribe(change => {
|
||||||
|
const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API');
|
||||||
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
|
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
|
||||||
const chat_id = config_api.getConfigItem('ytdl_telegram_chat_id');
|
if (!use_telegram_api || !bot_token) return;
|
||||||
const bot = new TelegramBot(bot_token);
|
if (!change) return;
|
||||||
if (thumbnail) await bot.sendPhoto(chat_id, thumbnail);
|
if (change['key'] === 'ytdl_use_telegram_API' || change['key'] === 'ytdl_telegram_bot_token' || change['key'] === 'ytdl_telegram_webhook_proxy') {
|
||||||
bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
|
logger.debug('Telegram bot setting up');
|
||||||
|
setupTelegramBot();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setupTelegramBot() {
|
||||||
|
const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API');
|
||||||
|
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
|
||||||
|
if (!use_telegram_api || !bot_token) return;
|
||||||
|
|
||||||
|
telegram_bot = new TelegramBotAPI(bot_token);
|
||||||
|
const webhook_proxy = config_api.getConfigItem('ytdl_telegram_webhook_proxy');
|
||||||
|
const webhook_url = webhook_proxy ? webhook_proxy : `${utils.getBaseURL()}/api/telegramRequest`;
|
||||||
|
telegram_bot.setWebHook(webhook_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.sendTelegramNotification = async ({body, title, type, url, thumbnail}) => {
|
||||||
|
if (!telegram_bot){
|
||||||
|
logger.error('Telegram bot not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chat_id = config_api.getConfigItem('ytdl_telegram_chat_id');
|
||||||
|
if (!chat_id){
|
||||||
|
logger.error('Telegram chat ID required!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.verbose('Sending notification to Telegram');
|
||||||
|
if (thumbnail) await telegram_bot.sendPhoto(chat_id, thumbnail);
|
||||||
|
telegram_bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discord
|
||||||
|
|
||||||
async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
||||||
const discord_webhook_url = config_api.getConfigItem('ytdl_discord_webhook_url');
|
const discord_webhook_url = config_api.getConfigItem('ytdl_discord_webhook_url');
|
||||||
@@ -177,6 +217,8 @@ async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slack
|
||||||
|
|
||||||
function sendSlackNotification({body, title, type, url, thumbnail}) {
|
function sendSlackNotification({body, title, type, url, thumbnail}) {
|
||||||
const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url');
|
const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url');
|
||||||
logger.verbose(`Sending slack notification to ${slack_webhook_url}`);
|
logger.verbose(`Sending slack notification to ${slack_webhook_url}`);
|
||||||
@@ -236,6 +278,8 @@ function sendSlackNotification({body, title, type, url, thumbnail}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic
|
||||||
|
|
||||||
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}`);
|
||||||
|
|||||||
3817
backend/package-lock.json
generated
3817
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@
|
|||||||
"command-exists": "^1.2.9",
|
"command-exists": "^1.2.9",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"config": "^3.2.3",
|
"config": "^3.2.3",
|
||||||
|
"execa": "^5.1.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"feed": "^4.2.2",
|
"feed": "^4.2.2",
|
||||||
@@ -43,7 +44,6 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lowdb": "^1.0.0",
|
"lowdb": "^1.0.0",
|
||||||
"md5": "^2.2.1",
|
"md5": "^2.2.1",
|
||||||
"mocha": "^9.2.2",
|
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mongodb": "^3.6.9",
|
"mongodb": "^3.6.9",
|
||||||
"multer": "1.4.5-lts.1",
|
"multer": "1.4.5-lts.1",
|
||||||
@@ -61,10 +61,13 @@
|
|||||||
"read-last-lines": "^1.7.2",
|
"read-last-lines": "^1.7.2",
|
||||||
"rxjs": "^7.3.0",
|
"rxjs": "^7.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
|
"tree-kill": "^1.2.2",
|
||||||
"unzipper": "^0.10.10",
|
"unzipper": "^0.10.10",
|
||||||
"uuidv4": "^6.2.13",
|
"uuid": "^9.0.1",
|
||||||
"winston": "^3.7.2",
|
"winston": "^3.7.2",
|
||||||
"xmlbuilder2": "^3.0.2",
|
"xmlbuilder2": "^3.0.2"
|
||||||
"youtube-dl": "^3.0.2"
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"mocha": "^10.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
|
const youtubedl_api = require('./youtube-dl');
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const archive_api = require('./archive');
|
const archive_api = require('./archive');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
@@ -39,7 +39,7 @@ exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => {
|
|||||||
exports.writeSubscriptionMetadata(sub);
|
exports.writeSubscriptionMetadata(sub);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
if (!sub.paused) exports.getVideosForSub(sub, user_uid);
|
if (!sub.paused) exports.getVideosForSub(sub.id);
|
||||||
} else {
|
} else {
|
||||||
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
||||||
}
|
}
|
||||||
@@ -63,29 +63,18 @@ async function getSubscriptionInfo(sub) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise(async resolve => {
|
let {callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
|
||||||
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async (err, output) => {
|
const {parsed_output, err} = await callback;
|
||||||
if (debugMode) {
|
|
||||||
logger.info('Subscribe: got info for subscription ' + sub.id);
|
|
||||||
}
|
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error(err.stderr);
|
logger.error(err.stderr);
|
||||||
resolve(false);
|
return false;
|
||||||
} else if (output) {
|
|
||||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
|
||||||
logger.verbose('Could not get info for ' + sub.id);
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < output.length; i++) {
|
|
||||||
let output_json = null;
|
|
||||||
try {
|
|
||||||
output_json = JSON.parse(output[i]);
|
|
||||||
} catch(e) {
|
|
||||||
output_json = null;
|
|
||||||
}
|
}
|
||||||
|
logger.verbose('Subscribe: got info for subscription ' + sub.id);
|
||||||
|
for (const output_json of parsed_output) {
|
||||||
if (!output_json) {
|
if (!output_json) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sub.name) {
|
if (!sub.name) {
|
||||||
if (sub.isPlaylist) {
|
if (sub.isPlaylist) {
|
||||||
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
|
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
|
||||||
@@ -101,17 +90,14 @@ async function getSubscriptionInfo(sub) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: get even more info
|
return true;
|
||||||
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.unsubscribe = async (sub, deleteMode, user_uid = null) => {
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.unsubscribe = async (sub_id, deleteMode, user_uid = null) => {
|
||||||
|
const sub = await exports.getSubscription(sub_id);
|
||||||
let basePath = null;
|
let basePath = null;
|
||||||
if (user_uid)
|
if (user_uid)
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
@@ -134,6 +120,7 @@ exports.unsubscribe = async (sub, deleteMode, user_uid = null) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await killSubDownloads(sub_id, true);
|
||||||
await db_api.removeRecord('subscriptions', {id: id});
|
await db_api.removeRecord('subscriptions', {id: id});
|
||||||
await db_api.removeAllRecords('files', {sub_id: id});
|
await db_api.removeAllRecords('files', {sub_id: id});
|
||||||
|
|
||||||
@@ -218,12 +205,76 @@ exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = nul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getVideosForSub = async (sub, user_uid = null) => {
|
let current_sub_index = 0; // To keep track of the current subscription
|
||||||
const latest_sub_obj = await exports.getSubscription(sub.id);
|
exports.watchSubscriptionsInterval = async () => {
|
||||||
if (!latest_sub_obj || latest_sub_obj['downloading']) {
|
const subscriptions_check_interval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
||||||
|
let parent_interval = setInterval(() => watchSubscriptions(), subscriptions_check_interval*1000);
|
||||||
|
watchSubscriptions();
|
||||||
|
config_api.config_updated.subscribe(change => {
|
||||||
|
if (!change) return;
|
||||||
|
if (change['key'] === 'ytdl_subscriptions_check_interval' || change['key'] === 'ytdl_multi_user_mode') {
|
||||||
|
current_sub_index = 0; // TODO: start after the last sub check
|
||||||
|
logger.verbose('Resetting sub check schedule due to config change');
|
||||||
|
clearInterval(parent_interval);
|
||||||
|
const new_interval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
||||||
|
parent_interval = setInterval(() => watchSubscriptions(), new_interval*1000);
|
||||||
|
watchSubscriptions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function watchSubscriptions() {
|
||||||
|
const subscription_ids = await getValidSubscriptionsToCheck();
|
||||||
|
if (subscription_ids.length === 0) {
|
||||||
|
logger.info('Skipping subscription check as no valid subscriptions exist.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkSubscription(subscription_ids[current_sub_index]);
|
||||||
|
current_sub_index = (current_sub_index + 1) % subscription_ids.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkSubscription(sub_id) {
|
||||||
|
let sub = await exports.getSubscription(sub_id);
|
||||||
|
|
||||||
|
// don't check the sub if the last check for the same subscription has not completed
|
||||||
|
if (sub.downloading) {
|
||||||
|
logger.verbose(`Subscription: skipped checking ${sub.name} as it's downloading videos.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sub.name) {
|
||||||
|
logger.verbose(`Subscription: skipped check for subscription with uid ${sub.id} as name has not been retrieved yet.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await exports.getVideosForSub(sub.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getValidSubscriptionsToCheck() {
|
||||||
|
const subscriptions = await exports.getAllSubscriptions();
|
||||||
|
|
||||||
|
if (!subscriptions) return;
|
||||||
|
|
||||||
|
// auto pause deprecated streamingOnly mode
|
||||||
|
const streaming_only_subs = subscriptions.filter(sub => sub.streamingOnly);
|
||||||
|
exports.updateSubscriptionPropertyMultiple(streaming_only_subs, {paused: true});
|
||||||
|
|
||||||
|
const valid_subscription_ids = subscriptions.filter(sub => !sub.paused && !sub.streamingOnly).map(sub => sub.id);
|
||||||
|
return valid_subscription_ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getVideosForSub = async (sub_id) => {
|
||||||
|
const sub = await exports.getSubscription(sub_id);
|
||||||
|
if (!sub || sub['downloading']) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getVideosForSub(sub);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _getVideosForSub(sub) {
|
||||||
|
const user_uid = sub['user_uid'];
|
||||||
updateSubscriptionProperty(sub, {downloading: true}, user_uid);
|
updateSubscriptionProperty(sub, {downloading: true}, user_uid);
|
||||||
|
|
||||||
// get basePath
|
// get basePath
|
||||||
@@ -241,10 +292,15 @@ exports.getVideosForSub = async (sub, user_uid = null) => {
|
|||||||
// get videos
|
// get videos
|
||||||
logger.verbose(`Subscription: getting list of videos to download for ${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 => {
|
let {child_process, callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
|
||||||
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
|
updateSubscriptionProperty(sub, {child_process: child_process}, user_uid);
|
||||||
// cleanup
|
const {parsed_output, err} = await callback;
|
||||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
updateSubscriptionProperty(sub, {downloading: false, child_process: null}, user_uid);
|
||||||
|
if (!parsed_output) {
|
||||||
|
logger.error('Subscription check failed!');
|
||||||
|
if (err) logger.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// remove temporary archive file if it exists
|
// remove temporary archive file if it exists
|
||||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||||
@@ -254,20 +310,8 @@ exports.getVideosForSub = async (sub, user_uid = null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||||
const parsed_output = utils.parseOutputJSON(output, err);
|
|
||||||
if (!parsed_output) {
|
|
||||||
logger.error('Subscription check failed!');
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const files_to_download = await handleOutputJSON(parsed_output, sub, user_uid);
|
const files_to_download = await handleOutputJSON(parsed_output, sub, user_uid);
|
||||||
resolve(files_to_download);
|
return files_to_download;
|
||||||
return;
|
|
||||||
});
|
|
||||||
}, err => {
|
|
||||||
logger.error(err);
|
|
||||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOutputJSON(output_jsons, sub, user_uid) {
|
async function handleOutputJSON(output_jsons, sub, user_uid) {
|
||||||
@@ -388,7 +432,7 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
downloadConfig.push('-r', rate_limit);
|
downloadConfig.push('-r', rate_limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
if (default_downloader === 'yt-dlp') {
|
if (default_downloader === 'yt-dlp') {
|
||||||
downloadConfig.push('--no-clean-info-json');
|
downloadConfig.push('--no-clean-info-json');
|
||||||
}
|
}
|
||||||
@@ -419,8 +463,37 @@ async function getFilesToDownload(sub, output_jsons) {
|
|||||||
return files_to_download;
|
return files_to_download;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.cancelCheckSubscription = async (sub_id) => {
|
||||||
|
const sub = await exports.getSubscription(sub_id);
|
||||||
|
if (!sub['downloading'] && !sub['child_process']) {
|
||||||
|
logger.error('Failed to cancel subscription check, verify that it is still running!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if check is ongoing
|
||||||
|
if (sub['child_process']) {
|
||||||
|
const child_process = sub['child_process'];
|
||||||
|
youtubedl_api.killYoutubeDLProcess(child_process);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel activate video downloads
|
||||||
|
await killSubDownloads(sub_id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function killSubDownloads(sub_id, remove_downloads = false) {
|
||||||
|
const sub_downloads = await db_api.getRecords('download_queue', {sub_id: sub_id});
|
||||||
|
for (const sub_download of sub_downloads) {
|
||||||
|
if (sub_download['running'])
|
||||||
|
await downloader_api.cancelDownload(sub_download['uid']);
|
||||||
|
if (remove_downloads)
|
||||||
|
await db_api.removeRecord('download_queue', {uid: sub_download['uid']});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.getSubscriptions = async (user_uid = null) => {
|
exports.getSubscriptions = async (user_uid = null) => {
|
||||||
|
// TODO: fix issue where the downloading property may not match getSubscription()
|
||||||
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,24 +572,22 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
|||||||
const downloadConfig = await generateArgsForSubscription(sub, user_uid, true, new_path);
|
const downloadConfig = await generateArgsForSubscription(sub, user_uid, true, new_path);
|
||||||
logger.verbose(`Checking if a better version of the fresh upload ${file_obj['id']} exists.`);
|
logger.verbose(`Checking if a better version of the fresh upload ${file_obj['id']} exists.`);
|
||||||
// simulate a download to verify that a better version exists
|
// simulate a download to verify that a better version exists
|
||||||
youtubedl.getInfo(file_obj['url'], downloadConfig, async (err, output) => {
|
|
||||||
if (err) {
|
const info = await downloader_api.getVideoInfoByURL(file_obj['url'], downloadConfig);
|
||||||
// video is not available anymore for whatever reason
|
if (info && info.length === 1) {
|
||||||
} else if (output) {
|
|
||||||
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
|
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
|
||||||
if (output[metric_to_compare] > file_obj[metric_to_compare]) {
|
if (info[metric_to_compare] > file_obj[metric_to_compare]) {
|
||||||
// download new video as the simulated one is better
|
// download new video as the simulated one is better
|
||||||
youtubedl.exec(file_obj['url'], downloadConfig, {maxBuffer: Infinity}, async (err, output) => {
|
let {callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
|
||||||
|
const {parsed_output, err} = await callback;
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
|
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
|
||||||
} else if (output) {
|
} else if (parsed_output) {
|
||||||
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${output[metric_to_compare]}`);
|
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${info[metric_to_compare]}`);
|
||||||
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]});
|
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: info[metric_to_compare]});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false});
|
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const CONSTS = require('./consts');
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const scheduler = require('node-schedule');
|
const scheduler = require('node-schedule');
|
||||||
const { uuid } = require('uuidv4');
|
|
||||||
|
|
||||||
const TASKS = {
|
const TASKS = {
|
||||||
backup_local_db: {
|
backup_local_db: {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const low = require('lowdb')
|
|||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
const NodeID3 = require('node-id3');
|
||||||
const exec = util.promisify(require('child_process').exec);
|
const exec = util.promisify(require('child_process').exec);
|
||||||
|
|
||||||
const FileSync = require('lowdb/adapters/FileSync');
|
const FileSync = require('lowdb/adapters/FileSync');
|
||||||
@@ -44,9 +47,7 @@ const categories_api = require('../categories');
|
|||||||
const files_api = require('../files');
|
const files_api = require('../files');
|
||||||
const youtubedl_api = require('../youtube-dl');
|
const youtubedl_api = require('../youtube-dl');
|
||||||
const config_api = require('../config');
|
const config_api = require('../config');
|
||||||
const fs = require('fs-extra');
|
const CONSTS = require('../consts');
|
||||||
const { uuid } = require('uuidv4');
|
|
||||||
const NodeID3 = require('node-id3');
|
|
||||||
|
|
||||||
db_api.initialize(db, users_db, 'local_db_test.json');
|
db_api.initialize(db, users_db, 'local_db_test.json');
|
||||||
|
|
||||||
@@ -401,7 +402,7 @@ describe('Multi User', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Subscription zip generator', async function() {
|
it('Subscription zip generator', async function() {
|
||||||
const sub = await subscriptions_api.getSubscription(sub_to_test, user_to_test);
|
const sub = await subscriptions_api.getSubscription(sub_to_test.id, user_to_test);
|
||||||
const sub_videos = await db_api.getRecords('files', {sub_id: sub.id});
|
const sub_videos = await db_api.getRecords('files', {sub_id: sub.id});
|
||||||
assert(sub);
|
assert(sub);
|
||||||
const sub_files_to_download = [];
|
const sub_files_to_download = [];
|
||||||
@@ -441,11 +442,34 @@ describe('Multi User', async function() {
|
|||||||
describe('Downloader', function() {
|
describe('Downloader', function() {
|
||||||
const downloader_api = require('../downloader');
|
const downloader_api = require('../downloader');
|
||||||
const url = 'https://www.youtube.com/watch?v=hpigjnKl7nI';
|
const url = 'https://www.youtube.com/watch?v=hpigjnKl7nI';
|
||||||
|
const playlist_url = 'https://www.youtube.com/playlist?list=PLbZT16X07RLhqK-ZgSkRuUyiz9B_WLdNK';
|
||||||
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
||||||
const options = {
|
const options = {
|
||||||
ui_uid: uuid()
|
ui_uid: uuid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createCategory(url) {
|
||||||
|
// get info
|
||||||
|
const args = await downloader_api.generateArgs(url, 'video', options, null, true);
|
||||||
|
const [info] = await downloader_api.getVideoInfoByURL(url, args);
|
||||||
|
|
||||||
|
// create category
|
||||||
|
await db_api.removeAllRecords('categories');
|
||||||
|
const new_category = {
|
||||||
|
name: 'test_category',
|
||||||
|
uid: uuid(),
|
||||||
|
rules: [],
|
||||||
|
custom_output: ''
|
||||||
|
};
|
||||||
|
await db_api.insertRecordIntoTable('categories', new_category);
|
||||||
|
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
||||||
|
preceding_operator: null,
|
||||||
|
comparator: 'includes',
|
||||||
|
property: 'title',
|
||||||
|
value: info['title']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
before(async function() {
|
before(async function() {
|
||||||
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
|
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
|
||||||
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
|
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
|
||||||
@@ -455,6 +479,7 @@ describe('Downloader', function() {
|
|||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
// await db_api.connectToDB();
|
// await db_api.connectToDB();
|
||||||
await db_api.removeAllRecords('download_queue');
|
await db_api.removeAllRecords('download_queue');
|
||||||
|
config_api.setConfigItem('ytdl_allow_playlist_categorization', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Get file info', async function() {
|
it('Get file info', async function() {
|
||||||
@@ -480,6 +505,32 @@ describe('Downloader', function() {
|
|||||||
assert(success);
|
assert(success);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Downloader - categorize', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
await createCategory(url);
|
||||||
|
// collect info
|
||||||
|
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
||||||
|
await downloader_api.collectInfo(returned_download['uid']);
|
||||||
|
assert(returned_download['category']);
|
||||||
|
assert(returned_download['category']['name'] === 'test_category');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Downloader - categorize playlist', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
await createCategory(playlist_url);
|
||||||
|
// collect info
|
||||||
|
const returned_download_pass = await downloader_api.createDownload(playlist_url, 'video', options);
|
||||||
|
await downloader_api.collectInfo(returned_download_pass['uid']);
|
||||||
|
assert(returned_download_pass['category']);
|
||||||
|
assert(returned_download_pass['category']['name'] === 'test_category');
|
||||||
|
|
||||||
|
// test with playlist categorization disabled
|
||||||
|
config_api.setConfigItem('ytdl_allow_playlist_categorization', false);
|
||||||
|
const returned_download_fail = await downloader_api.createDownload(playlist_url, 'video', options);
|
||||||
|
await downloader_api.collectInfo(returned_download_fail['uid']);
|
||||||
|
assert(!returned_download_fail['category']);
|
||||||
|
});
|
||||||
|
|
||||||
it('Tag file', async function() {
|
it('Tag file', async function() {
|
||||||
const success = await generateEmptyAudioFile('test/sample_mp3.mp3');
|
const success = await generateEmptyAudioFile('test/sample_mp3.mp3');
|
||||||
const audio_path = './test/sample_mp3.mp3';
|
const audio_path = './test/sample_mp3.mp3';
|
||||||
@@ -552,7 +603,7 @@ describe('Downloader', function() {
|
|||||||
});
|
});
|
||||||
describe('Twitch', async function () {
|
describe('Twitch', async function () {
|
||||||
const twitch_api = require('../twitch');
|
const twitch_api = require('../twitch');
|
||||||
const example_vod = '1710641401';
|
const example_vod = '1790315420';
|
||||||
it('Download VOD chat', async function() {
|
it('Download VOD chat', async function() {
|
||||||
this.timeout(300000);
|
this.timeout(300000);
|
||||||
if (!fs.existsSync('TwitchDownloaderCLI')) {
|
if (!fs.existsSync('TwitchDownloaderCLI')) {
|
||||||
@@ -574,6 +625,105 @@ describe('Downloader', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('youtube-dl', async function() {
|
||||||
|
beforeEach(async function () {
|
||||||
|
if (fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.unlinkSync(CONSTS.DETAILS_BIN_PATH);
|
||||||
|
await youtubedl_api.checkForYoutubeDLUpdate();
|
||||||
|
});
|
||||||
|
it('Check latest version', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
const original_fork = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
const latest_version = await youtubedl_api.getLatestUpdateVersion(original_fork);
|
||||||
|
assert(latest_version > CONSTS.OUTDATED_YOUTUBEDL_VERSION);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Update youtube-dl', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
const original_fork = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
const binary_path = path.join('test', 'test_binary');
|
||||||
|
for (const youtubedl_fork in youtubedl_api.youtubedl_forks) {
|
||||||
|
config_api.setConfigItem('ytdl_default_downloader', youtubedl_fork);
|
||||||
|
const latest_version = await youtubedl_api.checkForYoutubeDLUpdate();
|
||||||
|
await youtubedl_api.updateYoutubeDL(latest_version, binary_path);
|
||||||
|
assert(fs.existsSync(binary_path));
|
||||||
|
if (fs.existsSync(binary_path)) fs.unlinkSync(binary_path);
|
||||||
|
}
|
||||||
|
config_api.setConfigItem('ytdl_default_downloader', original_fork);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Run process', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
const downloader_api = require('../downloader');
|
||||||
|
const url = 'https://www.youtube.com/watch?v=hpigjnKl7nI';
|
||||||
|
const args = await downloader_api.generateArgs(url, 'video', {}, null, true);
|
||||||
|
const {child_process} = await youtubedl_api.runYoutubeDL(url, args);
|
||||||
|
assert(child_process);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Subscriptions', function() {
|
||||||
|
const new_sub = {
|
||||||
|
name: 'test_sub',
|
||||||
|
url: 'https://www.youtube.com/channel/UCzofo-P8yMMCOv8rsPfIR-g',
|
||||||
|
maxQuality: null,
|
||||||
|
id: uuid(),
|
||||||
|
user_uid: null,
|
||||||
|
type: 'video',
|
||||||
|
paused: true
|
||||||
|
};
|
||||||
|
beforeEach(async function() {
|
||||||
|
await db_api.removeAllRecords('subscriptions');
|
||||||
|
});
|
||||||
|
it('Subscribe', async function () {
|
||||||
|
const success = await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
assert(success);
|
||||||
|
const sub_exists = await db_api.getRecord('subscriptions', {id: new_sub['id']});
|
||||||
|
assert(sub_exists);
|
||||||
|
});
|
||||||
|
it('Unsubscribe', async function () {
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
await subscriptions_api.unsubscribe(new_sub);
|
||||||
|
const sub_exists = await db_api.getRecord('subscriptions', {id: new_sub['id']});
|
||||||
|
assert(!sub_exists);
|
||||||
|
});
|
||||||
|
it('Delete subscription file', async function () {
|
||||||
|
|
||||||
|
});
|
||||||
|
it('Get subscription by name', async function () {
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
const sub_by_name = await subscriptions_api.getSubscriptionByName('test_sub');
|
||||||
|
assert(sub_by_name);
|
||||||
|
});
|
||||||
|
it('Get subscriptions', async function() {
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
const subs = await subscriptions_api.getSubscriptions(null);
|
||||||
|
assert(subs && subs.length === 1);
|
||||||
|
});
|
||||||
|
it('Update subscription', async function () {
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
const sub_update = Object.assign({}, new_sub, {name: 'updated_name'});
|
||||||
|
await subscriptions_api.updateSubscription(sub_update);
|
||||||
|
const updated_sub = await db_api.getRecord('subscriptions', {id: new_sub['id']});
|
||||||
|
assert(updated_sub['name'] === 'updated_name');
|
||||||
|
});
|
||||||
|
it('Update subscription property', async function () {
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
const sub_update = Object.assign({}, new_sub, {name: 'updated_name'});
|
||||||
|
await subscriptions_api.updateSubscriptionPropertyMultiple([sub_update], {name: 'updated_name'});
|
||||||
|
const updated_sub = await db_api.getRecord('subscriptions', {id: new_sub['id']});
|
||||||
|
assert(updated_sub['name'] === 'updated_name');
|
||||||
|
});
|
||||||
|
it('Write subscription metadata', async function() {
|
||||||
|
const metadata_path = path.join('subscriptions', 'channels', 'test_sub', 'subscription_backup.json');
|
||||||
|
if (fs.existsSync(metadata_path)) fs.unlinkSync(metadata_path);
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
assert(fs.existsSync(metadata_path));
|
||||||
|
});
|
||||||
|
it('Fresh uploads', async function() {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Tasks', function() {
|
describe('Tasks', function() {
|
||||||
const tasks_api = require('../tasks');
|
const tasks_api = require('../tasks');
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
@@ -635,7 +785,7 @@ describe('Tasks', function() {
|
|||||||
const success = await generateEmptyVideoFile('test/sample_mp4.mp4');
|
const success = await generateEmptyVideoFile('test/sample_mp4.mp4');
|
||||||
|
|
||||||
// pre-test cleanup
|
// pre-test cleanup
|
||||||
await db_api.removeAllRecords('files', {title: 'Sample File'});
|
await db_api.removeAllRecords('files', {path: 'test/missing_file.mp4'});
|
||||||
if (fs.existsSync('video/sample_mp4.info.json')) fs.unlinkSync('video/sample_mp4.info.json');
|
if (fs.existsSync('video/sample_mp4.info.json')) fs.unlinkSync('video/sample_mp4.info.json');
|
||||||
if (fs.existsSync('video/sample_mp4.mp4')) fs.unlinkSync('video/sample_mp4.mp4');
|
if (fs.existsSync('video/sample_mp4.mp4')) fs.unlinkSync('video/sample_mp4.mp4');
|
||||||
|
|
||||||
@@ -792,7 +942,7 @@ describe('Categories', async function() {
|
|||||||
rules: [],
|
rules: [],
|
||||||
custom_output: ''
|
custom_output: ''
|
||||||
};
|
};
|
||||||
|
await db_api.removeAllRecords('categories', {name: 'test_category'});
|
||||||
await db_api.insertRecordIntoTable('categories', new_category);
|
await db_api.insertRecordIntoTable('categories', new_category);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -887,6 +1037,66 @@ describe('Categories', async function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Config', async function() {
|
||||||
|
it('findChangedConfigItems', async function() {
|
||||||
|
const old_config = {
|
||||||
|
"YoutubeDLMaterial": {
|
||||||
|
"test_object1": {
|
||||||
|
"test_prop1": true,
|
||||||
|
"test_prop2": false
|
||||||
|
},
|
||||||
|
"test_object2": {
|
||||||
|
"test_prop3": {
|
||||||
|
"test_prop3_1": true,
|
||||||
|
"test_prop3_2": false
|
||||||
|
},
|
||||||
|
"test_prop4": false
|
||||||
|
},
|
||||||
|
"test_object3": {
|
||||||
|
"test_prop5": {
|
||||||
|
"test_prop5_1": true,
|
||||||
|
"test_prop5_2": false
|
||||||
|
},
|
||||||
|
"test_prop6": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const new_config = {
|
||||||
|
"YoutubeDLMaterial": {
|
||||||
|
"test_object1": {
|
||||||
|
"test_prop1": false,
|
||||||
|
"test_prop2": false
|
||||||
|
},
|
||||||
|
"test_object2": {
|
||||||
|
"test_prop3": {
|
||||||
|
"test_prop3_1": false,
|
||||||
|
"test_prop3_2": false
|
||||||
|
},
|
||||||
|
"test_prop4": true
|
||||||
|
},
|
||||||
|
"test_object3": {
|
||||||
|
"test_prop5": {
|
||||||
|
"test_prop5_1": true,
|
||||||
|
"test_prop5_2": false
|
||||||
|
},
|
||||||
|
"test_prop6": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changes = config_api.findChangedConfigItems(old_config, new_config);
|
||||||
|
assert(changes[0]['key'] === 'test_prop1' && changes[0]['old_value'] === true && changes[0]['new_value'] === false);
|
||||||
|
assert(changes[1]['key'] === 'test_prop3' &&
|
||||||
|
changes[1]['old_value']['test_prop3_1'] === true &&
|
||||||
|
changes[1]['new_value']['test_prop3_1'] === false &&
|
||||||
|
changes[1]['old_value']['test_prop3_2'] === false &&
|
||||||
|
changes[1]['new_value']['test_prop3_2'] === false);
|
||||||
|
assert(changes[2]['key'] === 'test_prop4' && changes[2]['old_value'] === false && changes[2]['new_value'] === true);
|
||||||
|
assert(changes[3]['key'] === 'test_prop6' && changes[3]['old_value'] === false && changes[3]['new_value'] === true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const generateEmptyVideoFile = async (file_path) => {
|
const generateEmptyVideoFile = async (file_path) => {
|
||||||
if (fs.existsSync(file_path)) fs.unlinkSync(file_path);
|
if (fs.existsSync(file_path)) fs.unlinkSync(file_path);
|
||||||
return await exec(`ffmpeg -t 1 -f lavfi -i color=c=black:s=640x480 -c:v libx264 -tune stillimage -pix_fmt yuv420p "${file_path}"`);
|
return await exec(`ffmpeg -t 1 -f lavfi -i color=c=black:s=640x480 -c:v libx264 -tune stillimage -pix_fmt yuv420p "${file_path}"`);
|
||||||
|
|||||||
@@ -241,11 +241,6 @@ exports.addUIDsToCategory = (category, files) => {
|
|||||||
return files_that_match;
|
return files_that_match;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getCurrentDownloader = () => {
|
|
||||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
|
||||||
return details_json['downloader'];
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.recFindByExt = async (base, ext, files, result, recursive = true) => {
|
exports.recFindByExt = async (base, ext, files, result, recursive = true) => {
|
||||||
files = files || (await fs.readdir(base))
|
files = files || (await fs.readdir(base))
|
||||||
result = result || []
|
result = result || []
|
||||||
@@ -534,7 +529,9 @@ exports.parseOutputJSON = (output, err) => {
|
|||||||
let split_output = [];
|
let split_output = [];
|
||||||
// const output_jsons = [];
|
// const output_jsons = [];
|
||||||
if (err && !output) {
|
if (err && !output) {
|
||||||
if (!err.stderr.includes('This video is unavailable') && !err.stderr.includes('Private video')) {
|
const attempt_backup_errs = ['This video is unavailable', 'Private video', 'unavailable video'];
|
||||||
|
const attempt_backup = err.stderr ? attempt_backup_errs.some(err_msg => err.stderr.includes(err_msg)) : false;
|
||||||
|
if (!attempt_backup) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
logger.info('An error was encountered with at least one video, backup method will be used.')
|
logger.info('An error was encountered with at least one video, backup method will be used.')
|
||||||
|
|||||||
@@ -1,151 +1,167 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
const path = require('path');
|
||||||
|
const execa = require('execa');
|
||||||
|
const kill = require('tree-kill');
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const CONSTS = require('./consts');
|
const CONSTS = require('./consts');
|
||||||
const config_api = require('./config.js');
|
const config_api = require('./config.js');
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
const OUTDATED_VERSION = "2020.00.00";
|
|
||||||
|
|
||||||
const is_windows = process.platform === 'win32';
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
const download_sources = {
|
exports.youtubedl_forks = {
|
||||||
'youtube-dl': {
|
'youtube-dl': {
|
||||||
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags',
|
'download_url': 'https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl',
|
||||||
'func': downloadLatestYoutubeDLBinary
|
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags'
|
||||||
},
|
},
|
||||||
'youtube-dlc': {
|
'youtube-dlc': {
|
||||||
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags',
|
'download_url': 'https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc',
|
||||||
'func': downloadLatestYoutubeDLCBinary
|
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags'
|
||||||
},
|
},
|
||||||
'yt-dlp': {
|
'yt-dlp': {
|
||||||
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags',
|
'download_url': 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp',
|
||||||
'func': downloadLatestYoutubeDLPBinary
|
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.runYoutubeDL = async (url, args, downloadMethod = youtubedl.exec) => {
|
exports.runYoutubeDL = async (url, args, customDownloadHandler = null) => {
|
||||||
|
const output_file_path = getYoutubeDLPath();
|
||||||
|
if (!fs.existsSync(output_file_path)) await exports.checkForYoutubeDLUpdate();
|
||||||
|
let callback = null;
|
||||||
|
let child_process = null;
|
||||||
|
if (customDownloadHandler) {
|
||||||
|
callback = runYoutubeDLCustom(url, args, customDownloadHandler);
|
||||||
|
} else {
|
||||||
|
({callback, child_process} = await runYoutubeDLProcess(url, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {child_process, callback};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run youtube-dl directly (not cancellable)
|
||||||
|
const runYoutubeDLCustom = async (url, args, customDownloadHandler) => {
|
||||||
|
const downloadHandler = customDownloadHandler;
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
downloadMethod(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
downloadHandler(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
||||||
const parsed_output = utils.parseOutputJSON(output, err);
|
const parsed_output = utils.parseOutputJSON(output, err);
|
||||||
resolve({parsed_output, err});
|
resolve({parsed_output, err});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run youtube-dl in a subprocess (cancellable)
|
||||||
|
const runYoutubeDLProcess = async (url, args, youtubedl_fork = config_api.getConfigItem('ytdl_default_downloader')) => {
|
||||||
|
const youtubedl_path = getYoutubeDLPath(youtubedl_fork);
|
||||||
|
const binary_exists = fs.existsSync(youtubedl_path);
|
||||||
|
if (!binary_exists) {
|
||||||
|
const err = `Could not find path for ${youtubedl_fork} at ${youtubedl_path}`;
|
||||||
|
logger.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const child_process = execa(getYoutubeDLPath(youtubedl_fork), [url, ...args], {maxBuffer: Infinity});
|
||||||
|
const callback = new Promise(async resolve => {
|
||||||
|
try {
|
||||||
|
const {stdout, stderr} = await child_process;
|
||||||
|
const parsed_output = utils.parseOutputJSON(stdout.trim().split(/\r?\n/), stderr);
|
||||||
|
resolve({parsed_output, err: stderr});
|
||||||
|
} catch (e) {
|
||||||
|
// Attempt to not fail
|
||||||
|
const parsed_output = utils.parseOutputJSON(e && e.stdout && e.stdout.trim().split(/\r?\n/), e && e.stderr);
|
||||||
|
resolve({parsed_output: parsed_output, err: parsed_output ? null : e});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {child_process, callback}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getYoutubeDLPath(youtubedl_fork = config_api.getConfigItem('ytdl_default_downloader')) {
|
||||||
|
const binary_file_name = youtubedl_fork + (is_windows ? '.exe' : '');
|
||||||
|
const binary_path = path.join('appdata', 'bin', binary_file_name);
|
||||||
|
return binary_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.killYoutubeDLProcess = async (child_process) => {
|
||||||
|
kill(child_process.pid, 'SIGKILL');
|
||||||
|
}
|
||||||
|
|
||||||
exports.checkForYoutubeDLUpdate = async () => {
|
exports.checkForYoutubeDLUpdate = async () => {
|
||||||
return new Promise(async resolve => {
|
const selected_fork = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
const output_file_path = getYoutubeDLPath();
|
||||||
const tags_url = download_sources[default_downloader]['tags_url'];
|
|
||||||
// get current version
|
// get current version
|
||||||
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
|
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
|
||||||
if (!current_app_details_exists) {
|
if (!current_app_details_exists[selected_fork]) {
|
||||||
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
|
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
|
||||||
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version": OUTDATED_VERSION, "downloader": default_downloader});
|
updateDetailsJSON(CONSTS.OUTDATED_YOUTUBEDL_VERSION, selected_fork, output_file_path);
|
||||||
}
|
}
|
||||||
let current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
|
const current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
|
||||||
let current_version = current_app_details['version'];
|
const current_version = current_app_details[selected_fork]['version'];
|
||||||
let current_downloader = current_app_details['downloader'];
|
const current_fork = current_app_details[selected_fork]['downloader'];
|
||||||
let stored_binary_path = current_app_details['path'];
|
|
||||||
if (!stored_binary_path || typeof stored_binary_path !== 'string') {
|
const latest_version = await exports.getLatestUpdateVersion(selected_fork);
|
||||||
// logger.info(`INFO: Failed to get youtube-dl binary path at location: ${CONSTS.DETAILS_BIN_PATH}, attempting to guess actual path...`);
|
// if the binary does not exist, or default_downloader doesn't match existing fork, or if the fork has been updated, redownload
|
||||||
const guessed_base_path = 'node_modules/youtube-dl/bin/';
|
// TODO: don't redownload if fork already exists
|
||||||
const guessed_file_path = guessed_base_path + 'youtube-dl' + (is_windows ? '.exe' : '');
|
if (!fs.existsSync(output_file_path) || current_fork !== selected_fork || !current_version || current_version !== latest_version) {
|
||||||
if (fs.existsSync(guessed_file_path)) {
|
logger.warn(`Updating ${selected_fork} binary to '${output_file_path}', downloading...`);
|
||||||
stored_binary_path = guessed_file_path;
|
await exports.updateYoutubeDL(latest_version);
|
||||||
// logger.info('INFO: Guess successful! Update process continuing...')
|
}
|
||||||
} else {
|
}
|
||||||
logger.error(`Guess '${guessed_file_path}' is not correct. Cancelling update check. Verify that your youtube-dl binaries exist by running npm install.`);
|
|
||||||
resolve(null);
|
exports.updateYoutubeDL = async (latest_update_version, custom_output_path = null) => {
|
||||||
|
await fs.ensureDir(path.join('appdata', 'bin'));
|
||||||
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
await downloadLatestYoutubeDLBinaryGeneric(default_downloader, latest_update_version, custom_output_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadLatestYoutubeDLBinaryGeneric(youtubedl_fork, new_version, custom_output_path = null) {
|
||||||
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
|
||||||
|
// build the URL
|
||||||
|
const download_url = `${exports.youtubedl_forks[youtubedl_fork]['download_url']}${file_ext}`;
|
||||||
|
const output_path = custom_output_path || getYoutubeDLPath(youtubedl_fork);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await utils.fetchFile(download_url, output_path, `${youtubedl_fork} ${new_version}`);
|
||||||
|
fs.chmod(output_path, 0o777);
|
||||||
|
|
||||||
|
updateDetailsJSON(new_version, youtubedl_fork, output_path);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Failed to download new ${youtubedl_fork} version: ${new_version}`);
|
||||||
|
logger.error(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// got version, now let's check the latest version from the youtube-dl API
|
exports.getLatestUpdateVersion = async (youtubedl_fork) => {
|
||||||
|
const tags_url = exports.youtubedl_forks[youtubedl_fork]['tags_url'];
|
||||||
|
return new Promise(resolve => {
|
||||||
fetch(tags_url, {method: 'Get'})
|
fetch(tags_url, {method: 'Get'})
|
||||||
.then(async res => res.json())
|
.then(async res => res.json())
|
||||||
.then(async (json) => {
|
.then(async (json) => {
|
||||||
// check if the versions are different
|
|
||||||
if (!json || !json[0]) {
|
if (!json || !json[0]) {
|
||||||
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
logger.error(`Failed to check ${youtubedl_fork} version for an update.`)
|
||||||
resolve(null);
|
resolve(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const latest_update_version = json[0]['name'];
|
const latest_update_version = json[0]['name'];
|
||||||
if (current_version !== latest_update_version || default_downloader !== current_downloader) {
|
|
||||||
// versions different or different downloader is being used, download new update
|
|
||||||
resolve(latest_update_version);
|
resolve(latest_update_version);
|
||||||
} else {
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
logger.error(`Failed to check ${youtubedl_fork} version for an update.`)
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updateYoutubeDL = async (latest_update_version) => {
|
function updateDetailsJSON(new_version, fork, output_path) {
|
||||||
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
|
||||||
await download_sources[default_downloader]['func'](latest_update_version);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.verifyBinaryExistsLinux = () => {
|
|
||||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
|
||||||
if (!is_windows && details_json && (!details_json['path'] || details_json['path'].includes('.exe'))) {
|
|
||||||
details_json['path'] = 'node_modules/youtube-dl/bin/youtube-dl';
|
|
||||||
details_json['exec'] = 'youtube-dl';
|
|
||||||
details_json['version'] = OUTDATED_VERSION;
|
|
||||||
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
|
|
||||||
|
|
||||||
utils.restartServer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLBinary(new_version) {
|
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
const details_json = fs.existsSync(CONSTS.DETAILS_BIN_PATH) ? fs.readJSONSync(CONSTS.DETAILS_BIN_PATH) : {};
|
||||||
const download_url = `https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl${file_ext}`;
|
if (!details_json[fork]) details_json[fork] = {};
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
const fork_json = details_json[fork];
|
||||||
|
fork_json['version'] = new_version;
|
||||||
await utils.fetchFile(download_url, output_path, `youtube-dl ${new_version}`);
|
fork_json['downloader'] = fork;
|
||||||
|
fork_json['path'] = output_path; // unused
|
||||||
updateDetailsJSON(new_version, 'youtube-dl');
|
fork_json['exec'] = fork + file_ext; // unused
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLCBinary(new_version) {
|
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
|
||||||
|
|
||||||
const download_url = `https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc${file_ext}`;
|
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
|
||||||
|
|
||||||
await utils.fetchFile(download_url, output_path, `youtube-dlc ${new_version}`);
|
|
||||||
|
|
||||||
updateDetailsJSON(new_version, 'youtube-dlc');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLPBinary(new_version) {
|
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
|
||||||
|
|
||||||
const download_url = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp${file_ext}`;
|
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
|
||||||
|
|
||||||
await utils.fetchFile(download_url, output_path, `yt-dlp ${new_version}`);
|
|
||||||
|
|
||||||
updateDetailsJSON(new_version, 'yt-dlp');
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDetailsJSON(new_version, downloader) {
|
|
||||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
|
||||||
if (new_version) details_json['version'] = new_version;
|
|
||||||
details_json['downloader'] = downloader;
|
|
||||||
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
|
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: youtubedl-material
|
name: youtubedl-material
|
||||||
description: A Helm chart for Kubernetes
|
description: A Helm chart for https://github.com/Tzahi12345/YoutubeDL-Material
|
||||||
|
|
||||||
# A chart can be either an 'application' or a 'library' chart.
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
#
|
#
|
||||||
@@ -15,7 +15,7 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
{{- if .Values.ingress.enabled -}}
|
{{- if .Values.ingress.enabled -}}
|
||||||
{{- $fullName := include "youtubedl-material.fullname" . -}}
|
{{- $fullName := include "youtubedl-material.fullname" . -}}
|
||||||
{{- $svcPort := .Values.service.port -}}
|
{{- $svcPort := .Values.service.port -}}
|
||||||
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||||
|
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||||
|
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
apiVersion: networking.k8s.io/v1beta1
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
apiVersion: extensions/v1beta1
|
apiVersion: extensions/v1beta1
|
||||||
@@ -16,6 +23,9 @@ metadata:
|
|||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
|
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
{{- if .Values.ingress.tls }}
|
{{- if .Values.ingress.tls }}
|
||||||
tls:
|
tls:
|
||||||
{{- range .Values.ingress.tls }}
|
{{- range .Values.ingress.tls }}
|
||||||
@@ -33,9 +43,19 @@ spec:
|
|||||||
paths:
|
paths:
|
||||||
{{- range .paths }}
|
{{- range .paths }}
|
||||||
- path: {{ .path }}
|
- path: {{ .path }}
|
||||||
|
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
{{- end }}
|
||||||
backend:
|
backend:
|
||||||
|
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
port:
|
||||||
|
number: {{ $svcPort }}
|
||||||
|
{{- else }}
|
||||||
serviceName: {{ $fullName }}
|
serviceName: {{ $fullName }}
|
||||||
servicePort: {{ $svcPort }}
|
servicePort: {{ $svcPort }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ esac
|
|||||||
|
|
||||||
echo "(INFO) Architecture detected: $ARCH"
|
echo "(INFO) Architecture detected: $ARCH"
|
||||||
echo "(1/5) READY - Install unzip"
|
echo "(1/5) READY - Install unzip"
|
||||||
apt-get update && apt-get -y install unzip curl jq libicu70
|
apt-get update && apt-get -y install unzip curl jq
|
||||||
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')
|
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"
|
echo "(2/5) DOWNLOAD - Acquire twitchdownloader"
|
||||||
curl -o twitchdownloader.zip \
|
curl -o twitchdownloader.zip \
|
||||||
|
|||||||
20070
package-lock.json
generated
20070
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
53
package.json
53
package.json
@@ -5,6 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
|
"codespaces": "ng serve --configuration=codespaces",
|
||||||
"build": "ng build --configuration production",
|
"build": "ng build --configuration production",
|
||||||
"prebuild": "node src/postbuild.mjs",
|
"prebuild": "node src/postbuild.mjs",
|
||||||
"heroku-postbuild": "npm install --prefix backend",
|
"heroku-postbuild": "npm install --prefix backend",
|
||||||
@@ -16,23 +17,23 @@
|
|||||||
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n --out-file=messages.en.xlf"
|
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n --out-file=messages.en.xlf"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.3.1",
|
"node": "18.19.0",
|
||||||
"npm": "6.10.3"
|
"npm": "10.2.3"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "^15.0.1",
|
"@angular-devkit/core": "^17.0.5",
|
||||||
"@angular/animations": "^15.0.1",
|
"@angular/animations": "^17.0.5",
|
||||||
"@angular/cdk": "^15.0.0",
|
"@angular/cdk": "^17.0.2",
|
||||||
"@angular/common": "^15.0.1",
|
"@angular/common": "^17.0.5",
|
||||||
"@angular/compiler": "^15.0.1",
|
"@angular/compiler": "^17.0.5",
|
||||||
"@angular/core": "^15.0.1",
|
"@angular/core": "^17.0.5",
|
||||||
"@angular/forms": "^15.0.1",
|
"@angular/forms": "^17.0.5",
|
||||||
"@angular/localize": "^15.0.1",
|
"@angular/localize": "^17.0.5",
|
||||||
"@angular/material": "^15.0.0",
|
"@angular/material": "^17.0.2",
|
||||||
"@angular/platform-browser": "^15.0.1",
|
"@angular/platform-browser": "^17.0.5",
|
||||||
"@angular/platform-browser-dynamic": "^15.0.1",
|
"@angular/platform-browser-dynamic": "^17.0.5",
|
||||||
"@angular/router": "^15.0.1",
|
"@angular/router": "^17.0.5",
|
||||||
"@fontsource/material-icons": "^4.5.4",
|
"@fontsource/material-icons": "^4.5.4",
|
||||||
"@ngneat/content-loader": "^7.0.0",
|
"@ngneat/content-loader": "^7.0.0",
|
||||||
"@videogular/ngx-videogular": "^6.0.0",
|
"@videogular/ngx-videogular": "^6.0.0",
|
||||||
@@ -43,20 +44,19 @@
|
|||||||
"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.10.0",
|
||||||
"ngx-file-drop": "^15.0.0",
|
"ngx-file-drop": "^15.0.0",
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^6.6.3",
|
||||||
"rxjs-compat": "^6.6.7",
|
"rxjs-compat": "^6.6.7",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "~4.8.4",
|
|
||||||
"xliff-to-json": "^1.0.4",
|
"xliff-to-json": "^1.0.4",
|
||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^15.0.1",
|
"@angular-devkit/build-angular": "^17.0.5",
|
||||||
"@angular/cli": "^15.0.1",
|
"@angular/cli": "^17.0.5",
|
||||||
"@angular/compiler-cli": "^15.0.1",
|
"@angular/compiler-cli": "^17.0.5",
|
||||||
"@angular/language-service": "^15.0.1",
|
"@angular/language-service": "^17.0.5",
|
||||||
"@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": "^4.3.1",
|
"@types/jasmine": "^4.3.1",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"ajv": "^7.2.4",
|
"ajv": "^7.2.4",
|
||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"jasmine-core": "~3.6.0",
|
"jasmine-core": "~3.8.0",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
"karma": "~6.4.2",
|
"karma": "~6.4.2",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
@@ -77,6 +77,13 @@
|
|||||||
"openapi-typescript-codegen": "^0.23.0",
|
"openapi-typescript-codegen": "^0.23.0",
|
||||||
"protractor": "~7.0.0",
|
"protractor": "~7.0.0",
|
||||||
"ts-node": "~3.0.4",
|
"ts-node": "~3.0.4",
|
||||||
"tslint": "~6.1.0"
|
"tslint": "~6.1.0",
|
||||||
|
"typescript": "~5.2.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"ngx-avatars": {
|
||||||
|
"@angular/common": "^17.0.0",
|
||||||
|
"@angular/core": "^17.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type { ChangeRolePermissionsRequest } from './models/ChangeRolePermission
|
|||||||
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
|
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
|
||||||
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
|
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
|
||||||
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
|
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
|
||||||
|
export type { CheckSubscriptionRequest } from './models/CheckSubscriptionRequest';
|
||||||
export type { ClearDownloadsRequest } from './models/ClearDownloadsRequest';
|
export type { ClearDownloadsRequest } from './models/ClearDownloadsRequest';
|
||||||
export type { ConcurrentStream } from './models/ConcurrentStream';
|
export type { ConcurrentStream } from './models/ConcurrentStream';
|
||||||
export type { Config } from './models/Config';
|
export type { Config } from './models/Config';
|
||||||
|
|||||||
7
src/api-types/models/CheckSubscriptionRequest.ts
Normal file
7
src/api-types/models/CheckSubscriptionRequest.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type CheckSubscriptionRequest = {
|
||||||
|
sub_id: string;
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ export type Download = {
|
|||||||
running: boolean;
|
running: boolean;
|
||||||
finished: boolean;
|
finished: boolean;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
|
cancelled?: boolean;
|
||||||
finished_step: boolean;
|
finished_step: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ export type Subscription = {
|
|||||||
type: FileType;
|
type: FileType;
|
||||||
user_uid: string | null;
|
user_uid: string | null;
|
||||||
isPlaylist: boolean;
|
isPlaylist: boolean;
|
||||||
|
child_process?: any;
|
||||||
archive?: string;
|
archive?: string;
|
||||||
timerange?: string;
|
timerange?: string;
|
||||||
custom_args?: string;
|
custom_args?: string;
|
||||||
custom_output?: string;
|
custom_output?: string;
|
||||||
|
downloading?: boolean;
|
||||||
|
paused?: boolean;
|
||||||
videos: Array<any>;
|
videos: Array<any>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import type { SubscriptionRequestData } from './SubscriptionRequestData';
|
|
||||||
|
|
||||||
export type UnsubscribeRequest = {
|
export type UnsubscribeRequest = {
|
||||||
sub: SubscriptionRequestData;
|
sub_id: string;
|
||||||
/**
|
/**
|
||||||
* Defaults to false
|
* Defaults to false
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,13 +5,23 @@
|
|||||||
<div class="row" width="100%" height="100%">
|
<div class="row" width="100%" height="100%">
|
||||||
<div class="col-6" style="text-align: left; margin-top: 1px;">
|
<div class="col-6" style="text-align: left; margin-top: 1px;">
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<button #hamburgerMenu style="outline: none" *ngIf="router.url.split(';')[0] !== '/player'" mat-icon-button aria-label="Toggle side navigation" (click)="toggleSidenav()"><mat-icon>menu</mat-icon></button>
|
@if (router.url.split(';')[0] !== '/player') {
|
||||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
<button #hamburgerMenu style="outline: none" mat-icon-button aria-label="Toggle side navigation" (click)="toggleSidenav()"><mat-icon>menu</mat-icon></button>
|
||||||
|
} @else {
|
||||||
|
<button (click)="goBack()" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||||
|
}
|
||||||
<div style="margin-left: 8px; display: inline-block;"><button mat-icon-button routerLink='/home'><img style="width: 32px;" src="assets/images/logo_128px.png"></button></div>
|
<div style="margin-left: 8px; display: inline-block;"><button mat-icon-button routerLink='/home'><img style="width: 32px;" src="assets/images/logo_128px.png"></button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6" style="text-align: right; align-items: flex-end; display: inline-block">
|
<div class="col-6" style="text-align: right; align-items: flex-end; display: inline-block">
|
||||||
<button *ngIf="postsService.config?.Extra.enable_notifications" [matMenuTriggerFor]="notificationsMenu" (menuOpened)="notificationMenuOpened()" mat-icon-button><mat-icon [matBadge]="notification_count" matBadgeColor="warn" matBadgeSize="small" *ngIf="notification_count > 0">notifications</mat-icon><mat-icon *ngIf="notification_count === 0">notifications_none</mat-icon></button>
|
@if (postsService.config?.Extra.enable_notifications) {
|
||||||
|
<button [matMenuTriggerFor]="notificationsMenu" (menuOpened)="notificationMenuOpened()" mat-icon-button>
|
||||||
|
@if (notification_count > 0) {
|
||||||
|
<mat-icon [matBadge]="notification_count" matBadgeColor="warn" matBadgeSize="small">notifications</mat-icon>
|
||||||
|
} @else {
|
||||||
|
<mat-icon>notifications_none</mat-icon>
|
||||||
|
}</button>
|
||||||
|
}
|
||||||
<mat-menu [classList]="'notifications-menu'" (close)="notificationMenuClosed()" #notificationsMenu="matMenu">
|
<mat-menu [classList]="'notifications-menu'" (close)="notificationMenuClosed()" #notificationsMenu="matMenu">
|
||||||
<app-notifications #notifications (notificationCount)="notificationCountUpdate($event)" (click)="$event.stopPropagation()"></app-notifications>
|
<app-notifications #notifications (notificationCount)="notificationCountUpdate($event)" (click)="$event.stopPropagation()"></app-notifications>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
@@ -21,15 +31,19 @@
|
|||||||
<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?.Advanced.multi_user_mode || postsService.isLoggedIn" class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
|
@if (!postsService.config?.Advanced.multi_user_mode || postsService.isLoggedIn) {
|
||||||
|
<button class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
|
||||||
<mat-icon>topic</mat-icon>
|
<mat-icon>topic</mat-icon>
|
||||||
<span i18n="Archives menu label">Archives</span>
|
<span i18n="Archives menu label">Archives</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="top-menu-button" (click)="themeMenuItemClicked($event)" *ngIf="allowThemeChange" mat-menu-item>
|
}
|
||||||
|
@if (allowThemeChange) {
|
||||||
|
<button class="top-menu-button" (click)="themeMenuItemClicked($event)" mat-menu-item>
|
||||||
<mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon>
|
<mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon>
|
||||||
<span i18n="Dark mode toggle label">Dark</span>
|
<span i18n="Dark mode toggle label">Dark</span>
|
||||||
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
|
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
<button class="top-menu-button" (click)="openAboutDialog()" mat-menu-item>
|
<button class="top-menu-button" (click)="openAboutDialog()" mat-menu-item>
|
||||||
<mat-icon>info</mat-icon>
|
<mat-icon>info</mat-icon>
|
||||||
<span i18n="About menu label">About</span>
|
<span i18n="About menu label">About</span>
|
||||||
@@ -44,19 +58,33 @@
|
|||||||
<mat-sidenav-container style="height: 100%">
|
<mat-sidenav-container style="height: 100%">
|
||||||
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #sidenav>
|
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #sidenav>
|
||||||
<mat-nav-list>
|
<mat-nav-list>
|
||||||
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
|
@if (postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)) {
|
||||||
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
|
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
|
||||||
<a *ngIf="postsService.config && allowSubscriptions && postsService.hasPermission('subscriptions')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
}
|
||||||
<a *ngIf="postsService.config && enableDownloadsManager && postsService.hasPermission('downloads_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
@if (postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn) {
|
||||||
<a *ngIf="postsService.config && postsService.hasPermission('tasks_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/tasks'><ng-container i18n="Navigation menu Tasks Page title">Tasks</ng-container></a>
|
<a mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
|
||||||
<ng-container *ngIf="postsService.config && postsService.hasPermission('settings')">
|
}
|
||||||
|
@if (postsService.config && allowSubscriptions && postsService.hasPermission('subscriptions')) {
|
||||||
|
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
||||||
|
}
|
||||||
|
@if (postsService.config && enableDownloadsManager && postsService.hasPermission('downloads_manager')) {
|
||||||
|
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
||||||
|
}
|
||||||
|
@if (postsService.config && postsService.hasPermission('tasks_manager')) {
|
||||||
|
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/tasks'><ng-container i18n="Navigation menu Tasks Page title">Tasks</ng-container></a>
|
||||||
|
}
|
||||||
|
@if (postsService.config && postsService.hasPermission('settings')) {
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/settings'><ng-container i18n="Settings menu label">Settings</ng-container></a>
|
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/settings'><ng-container i18n="Settings menu label">Settings</ng-container></a>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && postsService.hasPermission('subscriptions')">
|
@if (postsService.config && allowSubscriptions && postsService.subscriptions && postsService.hasPermission('subscriptions')) {
|
||||||
<mat-divider *ngIf="postsService.subscriptions.length > 0"></mat-divider>
|
@if (postsService.subscriptions.length > 0) {
|
||||||
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatars [style.display]="'inline-block'" [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatars>{{subscription.name}}</a>
|
<mat-divider></mat-divider>
|
||||||
</ng-container>
|
}
|
||||||
|
@for (subscription of postsService.subscriptions; track subscription) {
|
||||||
|
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatars [style.display]="'inline-block'" [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatars>{{subscription.name}}</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
|
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
|
||||||
|
|||||||
@@ -3,11 +3,9 @@
|
|||||||
<mat-label i18n="Filter">Filter</mat-label>
|
<mat-label i18n="Filter">Filter</mat-label>
|
||||||
<input matInput [(ngModel)]="text_filter" (keyup)="applyFilter($event)" #input>
|
<input matInput [(ngModel)]="text_filter" (keyup)="applyFilter($event)" #input>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<div [hidden]="!(archives && archives.length > 0)">
|
<div [hidden]="!(archives && archives.length > 0)">
|
||||||
<div class="mat-elevation-z8">
|
<div class="mat-elevation-z8">
|
||||||
<mat-table matSort [dataSource]="dataSource">
|
<mat-table matSort [dataSource]="dataSource">
|
||||||
|
|
||||||
<!-- Select Column -->
|
<!-- Select Column -->
|
||||||
<!-- Checkbox Column -->
|
<!-- Checkbox Column -->
|
||||||
<ng-container matColumnDef="select">
|
<ng-container matColumnDef="select">
|
||||||
@@ -25,13 +23,11 @@
|
|||||||
<mat-icon class="audio-video-icon">{{(row.type === 'audio') ? 'audiotrack' : 'movie'}}</mat-icon>
|
<mat-icon class="audio-video-icon">{{(row.type === 'audio') ? 'audiotrack' : 'movie'}}</mat-icon>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Date Column -->
|
<!-- Date Column -->
|
||||||
<ng-container matColumnDef="timestamp">
|
<ng-container matColumnDef="timestamp">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Date">Date</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Date">Date</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element"> {{element.timestamp*1000 | date: 'short'}} </mat-cell>
|
<mat-cell *matCellDef="let element"> {{element.timestamp*1000 | date: 'short'}} </mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- 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> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
|
||||||
@@ -41,7 +37,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- ID Column -->
|
<!-- ID Column -->
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="ID">ID</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="ID">ID</ng-container> </mat-header-cell>
|
||||||
@@ -51,7 +46,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Extractor Column -->
|
<!-- Extractor Column -->
|
||||||
<ng-container matColumnDef="extractor">
|
<ng-container matColumnDef="extractor">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Extractor">Extractor</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Extractor">Extractor</ng-container> </mat-header-cell>
|
||||||
@@ -61,17 +55,16 @@
|
|||||||
</span>
|
</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
|
||||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if ((!archives || archives.length === 0)) {
|
||||||
<div *ngIf="(!archives || archives.length === 0)">
|
<div>
|
||||||
<h4 style="text-align: center; margin-top: 10px;" i18n="Archives empty">Archives empty</h4>
|
<h4 style="text-align: center; margin-top: 10px;" i18n="Archives empty">Archives empty</h4>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div style="margin: 10px 10px 10px 0px; display: flex;">
|
<div style="margin: 10px 10px 10px 0px; display: flex;">
|
||||||
<span style="flex-grow: 1;" class="flex-items">
|
<span style="flex-grow: 1;" class="flex-items">
|
||||||
<button [disabled]="selection.selected.length === 0" color="warn" style="margin: 10px;" mat-stroked-button i18n="Delete selected" (click)="openDeleteSelectedArchivesDialog()">Delete selected</button>
|
<button [disabled]="selection.selected.length === 0" color="warn" style="margin: 10px;" mat-stroked-button i18n="Delete selected" (click)="openDeleteSelectedArchivesDialog()">Delete selected</button>
|
||||||
@@ -82,7 +75,9 @@
|
|||||||
<mat-label i18n="Subscription">Subscription</mat-label>
|
<mat-label i18n="Subscription">Subscription</mat-label>
|
||||||
<mat-select [ngModel]="sub_id" (ngModelChange)="subFilterSelectionChanged($event)">
|
<mat-select [ngModel]="sub_id" (ngModelChange)="subFilterSelectionChanged($event)">
|
||||||
<mat-option [value]="'none'" i18n="None">None</mat-option>
|
<mat-option [value]="'none'" i18n="None">None</mat-option>
|
||||||
<mat-option *ngFor="let sub of postsService.subscriptions" [value]="sub.id">{{sub.name}}</mat-option>
|
@for (sub of postsService.subscriptions; track sub) {
|
||||||
|
<mat-option [value]="sub.id">{{sub.name}}</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field style="width: 100px; margin-bottom: -1.25em; margin-left: 10px;">
|
<mat-form-field style="width: 100px; margin-bottom: -1.25em; margin-left: 10px;">
|
||||||
@@ -95,7 +90,6 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="file-drop-parent">
|
<div class="file-drop-parent">
|
||||||
<ngx-file-drop [multiple]="false" accept=".txt" dropZoneLabel="Drop file here" (onFileDrop)="dropped($event)">
|
<ngx-file-drop [multiple]="false" accept=".txt" dropZoneLabel="Drop file here" (onFileDrop)="dropped($event)">
|
||||||
<ng-template class="file-drop" ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
<ng-template class="file-drop" ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
||||||
@@ -110,11 +104,11 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</ngx-file-drop>
|
</ngx-file-drop>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 10px; color: white">
|
<div style="margin-top: 10px; color: white">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody class="upload-name-style">
|
<tbody class="upload-name-style">
|
||||||
<tr *ngFor="let item of files; let i=index">
|
@for (item of files; track item; let i = $index) {
|
||||||
|
<tr>
|
||||||
<td style="vertical-align: middle; border-top: unset">
|
<td style="vertical-align: middle; border-top: unset">
|
||||||
<strong>{{ item.relativePath }}</strong>
|
<strong>{{ item.relativePath }}</strong>
|
||||||
</td>
|
</td>
|
||||||
@@ -124,7 +118,9 @@
|
|||||||
<mat-label i18n="Subscription">Subscription</mat-label>
|
<mat-label i18n="Subscription">Subscription</mat-label>
|
||||||
<mat-select [ngModel]="upload_sub_id" (ngModelChange)="subUploadFilterSelectionChanged($event)">
|
<mat-select [ngModel]="upload_sub_id" (ngModelChange)="subUploadFilterSelectionChanged($event)">
|
||||||
<mat-option [value]="'none'" i18n="None">None</mat-option>
|
<mat-option [value]="'none'" i18n="None">None</mat-option>
|
||||||
<mat-option *ngFor="let sub of postsService.subscriptions" [value]="sub.id">{{sub.name}}</mat-option>
|
@for (sub of postsService.subscriptions; track sub) {
|
||||||
|
<mat-option [value]="sub.id">{{sub.name}}</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field style="width: 100px; margin-left: 10px">
|
<mat-form-field style="width: 100px; margin-left: 10px">
|
||||||
@@ -134,10 +130,15 @@
|
|||||||
<mat-option [value]="'audio'" i18n="Audio">Audio</mat-option>
|
<mat-option [value]="'audio'" i18n="Audio">Audio</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button style="margin-left: 10px" [disabled]="uploading_archive || uploaded_archive" (click)="importArchive()" matTooltip="Upload" i18n-matTooltip="Upload" mat-mini-fab><mat-icon>publish</mat-icon><mat-spinner *ngIf="uploading_archive" class="spinner" [diameter]="38"></mat-spinner></button>
|
<button style="margin-left: 10px" [disabled]="uploading_archive || uploaded_archive" (click)="importArchive()" matTooltip="Upload" i18n-matTooltip="Upload" mat-mini-fab><mat-icon>publish</mat-icon>
|
||||||
|
@if (uploading_archive) {
|
||||||
|
<mat-spinner class="spinner" [diameter]="38"></mat-spinner>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Archive } from 'api-types/models/Archive';
|
|||||||
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
|
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { NgxFileDropEntry } from 'ngx-file-drop';
|
import { NgxFileDropEntry } from 'ngx-file-drop';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-archive-viewer',
|
selector: 'app-archive-viewer',
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
<div class="buttons-container">
|
<div class="buttons-container">
|
||||||
<button (click)="startWatching()" *ngIf="!watch_together_clicked" mat-flat-button>Watch together</button>
|
@if (!watch_together_clicked) {
|
||||||
<button (click)="startServer()" *ngIf="watch_together_clicked && !started && server_mode && server_already_exists === false" mat-flat-button>Start stream</button>
|
<button (click)="startWatching()" mat-flat-button>Watch together</button>
|
||||||
<button (click)="startClient()" *ngIf="watch_together_clicked && !started && server_already_exists === true" mat-flat-button>Join stream</button>
|
} @else {
|
||||||
<button style="margin-left: 10px;" (click)="stop()" *ngIf="watch_together_clicked" mat-flat-button>Stop</button>
|
@if (!started) {
|
||||||
|
@if (server_already_exists) {
|
||||||
|
<button (click)="startClient()" mat-flat-button>Join stream</button>
|
||||||
|
} @else if (server_mode) {
|
||||||
|
<button (click)="startServer()" mat-flat-button>Start stream</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<button style="margin-left: 10px;" (click)="stop()" mat-flat-button>Stop</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
<div *ngIf="playlists && playlists.length > 0">
|
@if (playlists) {
|
||||||
|
<div>
|
||||||
<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' : '' ]">
|
@for (playlist of playlists; track playlist; let i = $index) {
|
||||||
|
<div 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)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''" [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>
|
} @empty {
|
||||||
</div>
|
<div style="text-align: center;">
|
||||||
</div>
|
|
||||||
<div *ngIf="playlists && playlists.length === 0" style="text-align: center;">
|
|
||||||
No playlists available. Create one from your downloading files by clicking the blue plus button.
|
No playlists available. Create one from your downloading files by clicking the blue plus button.
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog()" mat-fab><mat-icon>add</mat-icon></button></div>
|
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog()" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||||
@@ -4,6 +4,7 @@ import { Router } from '@angular/router';
|
|||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
||||||
import { Playlist } from 'api-types';
|
import { Playlist } from 'api-types';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-custom-playlists',
|
selector: 'app-custom-playlists',
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
<div [hidden]="!(downloads && downloads.length > 0)">
|
<div [hidden]="!(downloads && downloads.length > 0)">
|
||||||
<div style="overflow: hidden;" [ngClass]="uids ? 'rounded mat-elevation-z2' : 'mat-elevation-z8'">
|
<div style="overflow: hidden;" [ngClass]="uids ? 'rounded mat-elevation-z2' : 'mat-elevation-z8'">
|
||||||
<mat-table style="overflow: hidden" [ngClass]="uids ? 'rounded-top' : null" matSort [dataSource]="dataSource">
|
<mat-table style="overflow: hidden" [ngClass]="uids ? 'rounded-top' : null" matSort [dataSource]="dataSource">
|
||||||
|
|
||||||
<!-- Date Column -->
|
<!-- Date Column -->
|
||||||
<ng-container matColumnDef="timestamp_start">
|
<ng-container matColumnDef="timestamp_start">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Date">Date</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Date">Date</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element"> {{element.timestamp_start | date: 'short'}} </mat-cell>
|
<mat-cell *matCellDef="let element"> {{element.timestamp_start | date: 'short'}} </mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Title Column -->
|
<!-- Title Column -->
|
||||||
<ng-container matColumnDef="title">
|
<ng-container matColumnDef="title">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header style="flex: 2"> <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>
|
||||||
@@ -17,81 +15,88 @@
|
|||||||
</span>
|
</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Subscription Column -->
|
<!-- Subscription Column -->
|
||||||
<ng-container matColumnDef="sub_name">
|
<ng-container matColumnDef="sub_name">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Subscription">Subscription</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Subscription">Subscription</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element">
|
||||||
<ng-container *ngIf="element.sub_name">
|
@if (element.sub_name) {
|
||||||
{{element.sub_name}}
|
{{element.sub_name}}
|
||||||
</ng-container>
|
} @else {
|
||||||
<ng-container *ngIf="!element.sub_name">
|
<ng-container i18n="N/A">N/A</ng-container>
|
||||||
N/A
|
}
|
||||||
</ng-container>
|
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</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">
|
@if (!element.error) {
|
||||||
|
@if (element.step_index !== 2) {
|
||||||
{{STEP_INDEX_TO_LABEL[element.step_index]}}
|
{{STEP_INDEX_TO_LABEL[element.step_index]}}
|
||||||
</ng-container>
|
} @else {
|
||||||
<ng-container *ngIf="!element.error && element.step_index === 2">
|
@if (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>
|
} @else {
|
||||||
<ng-container *ngIf="!element.percent_complete">
|
<ng-container i18n="N/A">N/A</ng-container>
|
||||||
N/A
|
}
|
||||||
</ng-container>
|
}
|
||||||
</ng-container>
|
} @else {
|
||||||
<ng-container *ngIf="element.error" i18n="Error">Error</ng-container>
|
<ng-container 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 [ngStyle]="{flex: actionsFlex}"> <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" [ngStyle]="{flex: actionsFlex}">
|
<mat-cell *matCellDef="let element" [ngStyle]="{flex: actionsFlex}">
|
||||||
<div *ngIf="!minimizeButtons">
|
@if (!minimizeButtons) {
|
||||||
<ng-container *ngFor="let downloadAction of downloadActions">
|
<div>
|
||||||
|
@for (downloadAction of downloadActions; track downloadAction) {
|
||||||
<span class="button-span">
|
<span class="button-span">
|
||||||
<mat-spinner [diameter]="28" *ngIf="downloadAction.loading && downloadAction.loading(element)" class="icon-button-spinner"></mat-spinner>
|
@if (downloadAction.loading && downloadAction.loading(element)) {
|
||||||
<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>
|
<mat-spinner [diameter]="28" class="icon-button-spinner"></mat-spinner>
|
||||||
|
}
|
||||||
|
@if (downloadAction.show(element)) {
|
||||||
|
<button (click)="downloadAction.action(element)" [disabled]="downloadAction.loading && downloadAction.loading(element)" [matTooltip]="downloadAction.tooltip" mat-icon-button><mat-icon>{{downloadAction.icon}}</mat-icon></button>
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</ng-container>
|
}
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="minimizeButtons">
|
} @else {
|
||||||
|
<div>
|
||||||
<button [matMenuTriggerFor]="download_actions" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
<button [matMenuTriggerFor]="download_actions" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||||
<mat-menu #download_actions="matMenu">
|
<mat-menu #download_actions="matMenu">
|
||||||
<ng-container *ngFor="let downloadAction of downloadActions">
|
@for (downloadAction of downloadActions; track downloadAction) {
|
||||||
<button *ngIf="downloadAction.show(element)" (click)="downloadAction.action(element)" [disabled]="downloadAction.loading && downloadAction.loading(element)" mat-menu-item>
|
@if (downloadAction.show(element)) {
|
||||||
|
<button (click)="downloadAction.action(element)" [disabled]="downloadAction.loading && downloadAction.loading(element)" mat-menu-item>
|
||||||
<mat-icon>{{downloadAction.icon}}</mat-icon>
|
<mat-icon>{{downloadAction.icon}}</mat-icon>
|
||||||
<span>{{downloadAction.tooltip}}</span>
|
<span>{{downloadAction.tooltip}}</span>
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
}
|
||||||
|
}
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<mat-header-row [ngClass]="uids ? 'rounded-top' : null" *matHeaderRowDef="displayedColumns"></mat-header-row>
|
<mat-header-row [ngClass]="uids ? 'rounded-top' : null" *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
|
|
||||||
<mat-paginator [ngClass]="uids ? 'rounded-bottom' : null" [pageSizeOptions]="[5, 10, 20]"
|
<mat-paginator [ngClass]="uids ? 'rounded-bottom' : null" [pageSizeOptions]="[5, 10, 20]"
|
||||||
showFirstLastButtons
|
showFirstLastButtons
|
||||||
aria-label="Select page of downloads">
|
aria-label="Select page of downloads">
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!uids" class="downloads-action-button-div">
|
@if (!uids) {
|
||||||
|
<div class="downloads-action-button-div">
|
||||||
<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 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 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 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 class="downloads-action-button" color="warn" 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>
|
||||||
|
@if ((!downloads || downloads.length === 0) && downloads_retrieved && !uids) {
|
||||||
<div *ngIf="(!downloads || downloads.length === 0) && downloads_retrieved && !uids">
|
<div>
|
||||||
<h4 style="text-align: center; margin-top: 10px;" i18n="No downloads label">No downloads available!</h4>
|
<h4 style="text-align: center; margin-top: 10px;" i18n="No downloads label">No downloads available!</h4>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
@@ -69,8 +69,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
tooltip: $localize`Pause`,
|
tooltip: $localize`Pause`,
|
||||||
action: (download: Download) => this.pauseDownload(download),
|
action: (download: Download) => this.pauseDownload(download),
|
||||||
show: (download: Download) => !download.finished && (!download.paused || !download.finished_step),
|
show: (download: Download) => !download.finished && (!download.paused || !download.finished_step),
|
||||||
icon: 'pause',
|
icon: 'pause'
|
||||||
loading: (download: Download) => download.paused && !download.finished_step
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tooltip: $localize`Resume`,
|
tooltip: $localize`Resume`,
|
||||||
@@ -81,7 +80,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
{
|
{
|
||||||
tooltip: $localize`Cancel`,
|
tooltip: $localize`Cancel`,
|
||||||
action: (download: Download) => this.cancelDownload(download),
|
action: (download: Download) => this.cancelDownload(download),
|
||||||
show: (download: Download) => false && !download.finished && !download.paused, // TODO: add possibility to cancel download
|
show: (download: Download) => !download.finished && !download.paused && !download.cancelled,
|
||||||
icon: 'cancel'
|
icon: 'cancel'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<mat-tab *ngIf="registrationEnabled" label="Register" i18n-label="Register">
|
@if (registrationEnabled) {
|
||||||
|
<mat-tab label="Register" i18n-label="Register">
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<mat-form-field style="width: 100%">
|
<mat-form-field style="width: 100%">
|
||||||
<mat-label i18n="User name">User name</mat-label>
|
<mat-label i18n="User name">User name</mat-label>
|
||||||
@@ -34,13 +35,21 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
}
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
<div *ngIf="selectedTabIndex === 0" class="login-button-div">
|
@if (selectedTabIndex === 0) {
|
||||||
|
<div class="login-button-div">
|
||||||
<button [disabled]="loggingIn" color="primary" (click)="login()" mat-raised-button><ng-container i18n="Login">Login</ng-container></button>
|
<button [disabled]="loggingIn" color="primary" (click)="login()" mat-raised-button><ng-container i18n="Login">Login</ng-container></button>
|
||||||
<mat-progress-bar *ngIf="loggingIn" class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
|
@if (loggingIn) {
|
||||||
|
<mat-progress-bar class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="selectedTabIndex === 1" class="login-button-div">
|
} @else {
|
||||||
|
<div class="login-button-div">
|
||||||
<button [disabled]="registering" color="primary" (click)="register()" mat-raised-button><ng-container i18n="Register">Register</ng-container></button>
|
<button [disabled]="registering" color="primary" (click)="register()" mat-raised-button><ng-container i18n="Register">Register</ng-container></button>
|
||||||
<mat-progress-bar *ngIf="registering" class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
|
@if (registering) {
|
||||||
|
<mat-progress-bar class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
<div style="height: 100%;">
|
<div style="height: 100%;">
|
||||||
<div *ngIf="logs_loading" style="z-index: 999; position: absolute; top: 40%; left: 50%">
|
@if (logs_loading) {
|
||||||
|
<div style="z-index: 999; position: absolute; top: 40%; left: 50%">
|
||||||
<mat-spinner [diameter]="32"></mat-spinner>
|
<mat-spinner [diameter]="32"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<!-- Virtual mode (fast, select text buggy) -->
|
<!-- Virtual mode (fast, select text buggy) -->
|
||||||
<!--<cdk-virtual-scroll-viewport style="height: 274px;" itemSize="50" class="example-viewport">
|
<!--<cdk-virtual-scroll-viewport style="height: 274px;" itemSize="50" class="example-viewport">
|
||||||
<div *cdkVirtualFor="let log of logs; let i = index" class="example-item">
|
<div *cdkVirtualFor="let log of logs; let i = index" class="example-item">
|
||||||
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
|
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
|
||||||
</div>
|
</div>
|
||||||
</cdk-virtual-scroll-viewport>-->
|
</cdk-virtual-scroll-viewport>-->
|
||||||
|
|
||||||
<!-- Non-virtual mode (slow, bug-free) -->
|
<!-- Non-virtual mode (slow, bug-free) -->
|
||||||
<div style="height: 100%; overflow-y: auto">
|
<div style="height: 100%; overflow-y: auto">
|
||||||
<div *ngFor="let log of logs; let i = index" class="example-item">
|
@for (log of logs; track log) {
|
||||||
|
<div class="example-item">
|
||||||
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
|
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button style="position: absolute; right: 0px; top: 12px;" [cdkCopyToClipboard]="logs_text" (click)="copiedLogsToClipboard()" mat-mini-fab color="primary"><mat-icon style="font-size: 22px !important;">content_copy</mat-icon></button>
|
<button style="position: absolute; right: 0px; top: 12px;" [cdkCopyToClipboard]="logs_text" (click)="copiedLogsToClipboard()" mat-mini-fab color="primary"><mat-icon style="font-size: 22px !important;">content_copy</mat-icon></button>
|
||||||
<div style="display: inline-block;">
|
<div style="display: inline-block;">
|
||||||
@@ -33,5 +35,4 @@
|
|||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<button style="float: right; margin-top: 12px;" (click)="clearLogs()" mat-stroked-button color="warn"><ng-container i18n="Clear logs button">Clear logs</ng-container></button>
|
<button style="float: right; margin-top: 12px;" (click)="clearLogs()" mat-stroked-button color="warn"><ng-container i18n="Clear logs button">Clear logs</ng-container></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<h4 *ngIf="role" mat-dialog-title><ng-container i18n="Manage role dialog title">Manage role</ng-container> - {{role.key}}</h4>
|
@if (role) {
|
||||||
|
<h4 mat-dialog-title><ng-container i18n="Manage role dialog title">Manage role</ng-container> - {{role.key}}</h4>
|
||||||
<mat-dialog-content *ngIf="role">
|
<mat-dialog-content>
|
||||||
<div *ngFor="let permission of available_permissions">
|
@for (permission of available_permissions; track permission) {
|
||||||
|
<div>
|
||||||
<div matListItemTitle>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</div>
|
<div matListItemTitle>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</div>
|
||||||
<div matListItemLine>
|
<div matListItemLine>
|
||||||
<mat-radio-group [disabled]="permission === 'settings' && role.key === 'admin'" (change)="changeRolePermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
|
<mat-radio-group [disabled]="permission === 'settings' && role.key === 'admin'" (change)="changeRolePermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
|
||||||
@@ -10,8 +11,9 @@
|
|||||||
</mat-radio-group>
|
</mat-radio-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
}
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<h4 *ngIf="user" mat-dialog-title><ng-container i18n="Manage user dialog title">Manage user</ng-container> - {{user.name}}</h4>
|
@if (user) {
|
||||||
|
<h4 mat-dialog-title><ng-container i18n="Manage user dialog title">Manage user</ng-container> - {{user.name}}</h4>
|
||||||
<mat-dialog-content *ngIf="user">
|
<mat-dialog-content>
|
||||||
<p><ng-container i18n="User UID">User UID:</ng-container> {{user.uid}}</p>
|
<p><ng-container i18n="User UID">User UID:</ng-container> {{user.uid}}</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field style="margin-right: 15px;">
|
<mat-form-field style="margin-right: 15px;">
|
||||||
<mat-label i18n="New password">New password</mat-label>
|
<mat-label i18n="New password">New password</mat-label>
|
||||||
@@ -10,9 +9,9 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button mat-raised-button color="accent" (click)="setNewPassword()" [disabled]="newPasswordInput.length === 0"><ng-container i18n="Set new password">Set new password</ng-container></button>
|
<button mat-raised-button color="accent" (click)="setNewPassword()" [disabled]="newPasswordInput.length === 0"><ng-container i18n="Set new password">Set new password</ng-container></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div *ngFor="let permission of available_permissions">
|
@for (permission of available_permissions; track permission) {
|
||||||
|
<div>
|
||||||
<div matListItemTitle>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</div>
|
<div matListItemTitle>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</div>
|
||||||
<div matListItemLine>
|
<div matListItemLine>
|
||||||
<mat-radio-group [disabled]="permission === 'settings' && postsService.user.uid === user.uid" (change)="changeUserPermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give user permission for ' + permission">
|
<mat-radio-group [disabled]="permission === 'settings' && postsService.user.uid === user.uid" (change)="changeUserPermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give user permission for ' + permission">
|
||||||
@@ -22,9 +21,10 @@
|
|||||||
</mat-radio-group>
|
</mat-radio-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
}
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<div *ngIf="dataSource; else loading">
|
@if (dataSource) {
|
||||||
|
<div>
|
||||||
<div style="padding: 15px">
|
<div style="padding: 15px">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="table table-responsive pb-4 pt-4">
|
<div class="table table-responsive pb-4 pt-4">
|
||||||
@@ -8,34 +9,31 @@
|
|||||||
<input matInput (keyup)="applyFilter($event)">
|
<input matInput (keyup)="applyFilter($event)">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mat-elevation-z8" style="margin-right: 15px;">
|
<div class="mat-elevation-z8" style="margin-right: 15px;">
|
||||||
|
|
||||||
<mat-table #table [dataSource]="dataSource" matSort>
|
<mat-table #table [dataSource]="dataSource" matSort>
|
||||||
|
|
||||||
<!-- Name Column -->
|
<!-- Name Column -->
|
||||||
<ng-container matColumnDef="name">
|
<ng-container matColumnDef="name">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Username users table header"> User name </ng-container></mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Username users table header"> User name </ng-container></mat-header-cell>
|
||||||
|
|
||||||
<mat-cell *matCellDef="let row">
|
<mat-cell *matCellDef="let row">
|
||||||
<span *ngIf="editObject && editObject.uid === row.uid; else noteditingname">
|
@if (editObject && editObject.uid === row.uid) {
|
||||||
|
<span>
|
||||||
<span style="width: 80%;">
|
<span style="width: 80%;">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input matInput [(ngModel)]="constructedObject['name']" type="text" style="font-size: 12px">
|
<input matInput [(ngModel)]="constructedObject['name']" type="text" style="font-size: 12px">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<ng-template #noteditingname>
|
} @else {
|
||||||
{{row.name}}
|
{{row.name}}
|
||||||
</ng-template>
|
}
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Email Column -->
|
<!-- Email Column -->
|
||||||
<ng-container matColumnDef="role">
|
<ng-container matColumnDef="role">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Role users table header"> Role </ng-container></mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Role users table header"> Role </ng-container></mat-header-cell>
|
||||||
<mat-cell *matCellDef="let row">
|
<mat-cell *matCellDef="let row">
|
||||||
<span *ngIf="editObject && editObject.uid === row.uid; else noteditingemail">
|
@if (editObject && editObject.uid === row.uid) {
|
||||||
|
<span>
|
||||||
<span style="width: 80%;">
|
<span style="width: 80%;">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-select [(ngModel)]="constructedObject['role']">
|
<mat-select [(ngModel)]="constructedObject['role']">
|
||||||
@@ -45,17 +43,17 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<ng-template #noteditingemail>
|
} @else {
|
||||||
{{row.role}}
|
{{row.role}}
|
||||||
</ng-template>
|
}
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Actions Column -->
|
<!-- Actions Column -->
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Actions users table header"> Actions </ng-container></mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Actions users table header"> Actions </ng-container></mat-header-cell>
|
||||||
<mat-cell *matCellDef="let row">
|
<mat-cell *matCellDef="let row">
|
||||||
<span *ngIf="editObject && editObject.uid === row.uid; else notediting">
|
@if (editObject && editObject.uid === row.uid) {
|
||||||
|
<span>
|
||||||
<button mat-icon-button color="primary" (click)="finishEditing(row.uid)" matTooltip="Save" i18n-matTooltip="save user edit action button tooltip">
|
<button mat-icon-button color="primary" (click)="finishEditing(row.uid)" matTooltip="Save" i18n-matTooltip="save user edit action button tooltip">
|
||||||
<mat-icon>done</mat-icon>
|
<mat-icon>done</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
@@ -63,11 +61,11 @@
|
|||||||
<mat-icon>cancel</mat-icon>
|
<mat-icon>cancel</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<ng-template #notediting>
|
} @else {
|
||||||
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user" i18n-matTooltip="edit user action button tooltip">
|
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user" i18n-matTooltip="edit user action button tooltip">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</ng-template>
|
}
|
||||||
<button (click)="manageUser(row.uid)" mat-icon-button [disabled]="editObject && editObject.uid === row.uid" matTooltip="Manage user" i18n-matTooltip="manage user action button tooltip">
|
<button (click)="manageUser(row.uid)" mat-icon-button [disabled]="editObject && editObject.uid === row.uid" matTooltip="Manage user" i18n-matTooltip="manage user action button tooltip">
|
||||||
<mat-icon>settings</mat-icon>
|
<mat-icon>settings</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
@@ -76,17 +74,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
<mat-row *matRowDef="let row; columns: displayedColumns;">
|
<mat-row *matRowDef="let row; columns: displayedColumns;">
|
||||||
</mat-row>
|
</mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
|
|
||||||
<mat-paginator #paginator [length]="length"
|
<mat-paginator #paginator [length]="length"
|
||||||
[pageSize]="pageSize"
|
[pageSize]="pageSize"
|
||||||
[pageSizeOptions]="pageSizeOptions">
|
[pageSizeOptions]="pageSizeOptions">
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
|
||||||
<button color="primary" [disabled]="!this.users" mat-raised-button (click)="openAddUserDialog()" style="float: left; top: -45px; left: 15px">
|
<button color="primary" [disabled]="!this.users" mat-raised-button (click)="openAddUserDialog()" style="float: left; top: -45px; left: 15px">
|
||||||
<ng-container i18n="Add users button">Add Users</ng-container>
|
<ng-container i18n="Add users button">Add Users</ng-container>
|
||||||
</button>
|
</button>
|
||||||
@@ -95,14 +90,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<button color="primary" [matMenuTriggerFor]="edit_roles_menu" class="edit-role" mat-raised-button><ng-container i18n="Edit role">Edit Role</ng-container></button>
|
<button color="primary" [matMenuTriggerFor]="edit_roles_menu" class="edit-role" mat-raised-button><ng-container i18n="Edit role">Edit Role</ng-container></button>
|
||||||
<mat-menu #edit_roles_menu="matMenu">
|
<mat-menu #edit_roles_menu="matMenu">
|
||||||
<button (click)="openModifyRole(role)" mat-menu-item *ngFor="let role of roles">{{role.key}}</button>
|
@for (role of roles; track role) {
|
||||||
|
<button (click)="openModifyRole(role)" mat-menu-item>{{role.key}}</button>
|
||||||
|
}
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
} @else {
|
||||||
<div style="position: absolute" class="centered">
|
<div style="position: absolute" class="centered">
|
||||||
<ng-template #loading>
|
|
||||||
<mat-spinner></mat-spinner>
|
<mat-spinner></mat-spinner>
|
||||||
</ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,25 +8,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-card-subtitle>
|
</mat-card-subtitle>
|
||||||
<mat-card-title>
|
<mat-card-title>
|
||||||
<ng-container *ngIf="NOTIFICATION_PREFIX[notification.type]">
|
@if (NOTIFICATION_PREFIX[notification.type]) {
|
||||||
{{NOTIFICATION_PREFIX[notification.type]}}
|
{{NOTIFICATION_PREFIX[notification.type]}}
|
||||||
</ng-container>
|
}
|
||||||
</mat-card-title>
|
</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<ng-container *ngIf="NOTIFICATION_SUFFIX_KEY[notification.type]">
|
@if (NOTIFICATION_SUFFIX_KEY[notification.type]) {
|
||||||
<div style="word-break: break-word">
|
<div style="word-break: break-word">
|
||||||
{{notification['data'][NOTIFICATION_SUFFIX_KEY[notification.type]]}}
|
{{notification['data'][NOTIFICATION_SUFFIX_KEY[notification.type]]}}
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
}
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
<mat-card-actions class="notification-actions" *ngIf="notification.actions?.length > 0">
|
@if (notification.actions?.length > 0) {
|
||||||
|
<mat-card-actions class="notification-actions">
|
||||||
<button matTooltip="Remove" i18n-matTooltip="Remove" (click)="emitDeleteNotification(notification.uid)" mat-icon-button><mat-icon>close</mat-icon></button>
|
<button matTooltip="Remove" i18n-matTooltip="Remove" (click)="emitDeleteNotification(notification.uid)" mat-icon-button><mat-icon>close</mat-icon></button>
|
||||||
<span *ngFor="let action of notification.actions">
|
@for (action of notification.actions; track action) {
|
||||||
|
<span>
|
||||||
<button [matTooltip]="NOTIFICATION_ACTION_TO_STRING[action]" (click)="emitNotificationAction(notification, action)" mat-icon-button><mat-icon>{{NOTIFICATION_ICON[action]}}</mat-icon></button>
|
<button [matTooltip]="NOTIFICATION_ACTION_TO_STRING[action]" (click)="emitNotificationAction(notification, action)" mat-icon-button><mat-icon>{{NOTIFICATION_ICON[action]}}</mat-icon></button>
|
||||||
</span>
|
</span>
|
||||||
|
}
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
<span *ngIf="!notification.read" class="dot"></span>
|
}
|
||||||
|
@if (!notification.read) {
|
||||||
|
<span class="dot"></span>
|
||||||
|
}
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</cdk-virtual-scroll-viewport>
|
</cdk-virtual-scroll-viewport>
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
<div *ngIf="notifications !== null && notifications.length === 0" style="text-align: center; margin: 10px;" i18n="No notifications available">No notifications available</div>
|
@if (notifications !== null && notifications.length === 0) {
|
||||||
<div *ngIf="notifications?.length > 0">
|
<div style="text-align: center; margin: 10px;" i18n="No notifications available">No notifications available</div>
|
||||||
|
}
|
||||||
|
@if (notifications?.length > 0) {
|
||||||
|
<div>
|
||||||
<div class="notifications-list-parent">
|
<div class="notifications-list-parent">
|
||||||
<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>
|
@for (filter of notificationFilters | keyvalue: originalOrder; track filter) {
|
||||||
|
<mat-chip-option [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 class="notifications-list" [style.height]="list_height" (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;" color="warn" (click)="deleteAllNotifications()" mat-stroked-button>Remove all</button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Files title -->
|
<!-- Files title -->
|
||||||
<div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center">
|
<div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center">
|
||||||
<h4 *ngIf="!customHeader" class="my-videos-title" i18n="My files title">My files</h4>
|
@if (!customHeader) {
|
||||||
<h4 *ngIf="customHeader" class="my-videos-title">{{customHeader}}</h4>
|
<h4 class="my-videos-title" i18n="My files title">My files</h4>
|
||||||
|
} @else {
|
||||||
|
<h4 class="my-videos-title">{{customHeader}}</h4>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center">
|
<div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center">
|
||||||
@@ -21,57 +24,77 @@
|
|||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<mat-chip-listbox class="filter-list" [value]="selectedFilters" [multiple]="true" (change)="selectedFiltersChanged($event)">
|
<mat-chip-listbox class="filter-list" [value]="selectedFilters" [multiple]="true" (change)="selectedFiltersChanged($event)">
|
||||||
<mat-chip-option *ngFor="let filter of fileFilters | keyvalue: originalOrder" [value]="filter.key" [selected]="selectedFilters.includes(filter.key)" color="accent">{{filter.value.label}}</mat-chip-option>
|
@for (filter of fileFilters | keyvalue: originalOrder; track filter) {
|
||||||
|
<mat-chip-option [value]="filter.key" [selected]="selectedFilters.includes(filter.key)" color="accent">{{filter.value.label}}</mat-chip-option>
|
||||||
|
}
|
||||||
</mat-chip-listbox>
|
</mat-chip-listbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<!-- Files -->
|
<!-- Files -->
|
||||||
<div *ngIf="!selectMode" class="container" style="margin-bottom: 16px">
|
@if (!selectMode) {
|
||||||
|
<div class="container" style="margin-bottom: 16px">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<!-- Real cards -->
|
<!-- Real cards -->
|
||||||
<ng-container *ngIf="normal_files_received && paged_data">
|
@if (normal_files_received && paged_data) {
|
||||||
<div style="display: flex; align-items: center;" *ngFor="let file of paged_data; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
@for (file of paged_data; track file; let i = $index) {
|
||||||
|
<div style="display: flex; align-items: center;" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||||
<app-unified-file-card [ngClass]="downloading_content[file.uid] ? 'blurred' : ''" [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" (toggleFavorite)="toggleFavorite($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''"></app-unified-file-card>
|
<app-unified-file-card [ngClass]="downloading_content[file.uid] ? 'blurred' : ''" [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" (toggleFavorite)="toggleFavorite($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''"></app-unified-file-card>
|
||||||
<mat-spinner *ngIf="downloading_content[file.uid]" class="downloading-spinner" [diameter]="32"></mat-spinner>
|
@if (downloading_content[file.uid]) {
|
||||||
|
<mat-spinner class="downloading-spinner" [diameter]="32"></mat-spinner>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="paged_data.length === 0">
|
} @empty {
|
||||||
|
<div>
|
||||||
<ng-container i18n="No files found">No files found.</ng-container>
|
<ng-container i18n="No files found">No files found.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
}
|
||||||
|
}
|
||||||
<!-- Fake cards -->
|
<!-- Fake cards -->
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[normal_files_received ? 'hide' : '', 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' : '' ]">
|
@for (file of loading_files; track file; let i = $index) {
|
||||||
|
<div class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[normal_files_received ? 'hide' : '', postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
|
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
} @else {
|
||||||
<div *ngIf="selectMode">
|
<div>
|
||||||
<!-- If selected files e.g. for creating a playlist -->
|
<!-- If selected files e.g. for creating a playlist -->
|
||||||
<mat-tab-group [(selectedIndex)]="selectedIndex">
|
<mat-tab-group [(selectedIndex)]="selectedIndex">
|
||||||
<mat-tab label="Order" i18n-label="Order">
|
<mat-tab label="Order" i18n-label="Order">
|
||||||
<div *ngIf="selected_data.length">
|
@if (selected_data.length) {
|
||||||
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order </span>
|
<div>
|
||||||
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order </span>
|
@if (reverse_order === false) {
|
||||||
|
<span i18n="Normal order">Normal order </span>
|
||||||
|
}
|
||||||
|
@if (reverse_order === true) {
|
||||||
|
<span i18n="Reverse order">Reverse order </span>
|
||||||
|
}
|
||||||
<button (click)="toggleSelectionOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
|
<button (click)="toggleSelectionOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<!-- Selection order -->
|
<!-- Selection order -->
|
||||||
<mat-button-toggle-group *ngIf="selected_data.length" class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical #group="matButtonToggleGroup">
|
@if (selected_data.length) {
|
||||||
|
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical #group="matButtonToggleGroup">
|
||||||
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
|
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
|
||||||
<mat-button-toggle class="media-box" cdkDrag *ngFor="let file of (reverse_order ? selected_data_objs.slice().reverse() : selected_data_objs); let i = index" [checked]="false"><div><div class="playlist-item-text">{{file.title}}</div> <button (click)="removeSelectedFile(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
|
@for (file of (reverse_order ? selected_data_objs.slice().reverse() : selected_data_objs); track file; let i = $index) {
|
||||||
|
<mat-button-toggle class="media-box" cdkDrag [checked]="false"><div><div class="playlist-item-text">{{file.title}}</div> <button (click)="removeSelectedFile(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
|
||||||
|
}
|
||||||
</mat-button-toggle-group>
|
</mat-button-toggle-group>
|
||||||
|
} @else {
|
||||||
<div style="margin-top: 20px;" *ngIf="!selected_data.length">
|
<div style="margin-top: 20px;">
|
||||||
<h4 style="text-align: center;">No files selected!</h4>
|
<h4 style="text-align: center;">No files selected!</h4>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<mat-tab label="Select files" i18n-label="Select files">
|
<mat-tab label="Select files" i18n-label="Select files">
|
||||||
<mat-selection-list *ngIf="normal_files_received" (selectionChange)="fileSelectionChanged($event)">
|
@if (normal_files_received) {
|
||||||
<mat-list-option [selected]="selected_data.includes(file.uid)" *ngFor="let file of paged_data" [value]="file">
|
<mat-selection-list (selectionChange)="fileSelectionChanged($event)">
|
||||||
|
@for (file of paged_data; track file) {
|
||||||
|
<mat-list-option [selected]="selected_data.includes(file.uid)" [value]="file">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-10 select-file-title">
|
<div class="col-10 select-file-title">
|
||||||
@@ -81,25 +104,31 @@
|
|||||||
<div class="col-2">{{file.registered | date:'shortDate'}}</div>
|
<div class="col-2">{{file.registered | date:'shortDate'}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</mat-list-option>
|
</mat-list-option>
|
||||||
|
}
|
||||||
</mat-selection-list>
|
</mat-selection-list>
|
||||||
|
}
|
||||||
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
@if (!normal_files_received && loading_files && loading_files.length > 0) {
|
||||||
<mat-selection-list *ngIf="!normal_files_received">
|
@if (!normal_files_received) {
|
||||||
<mat-list-option *ngFor="let file of paged_data">
|
<mat-selection-list>
|
||||||
|
@for (file of paged_data; track file) {
|
||||||
|
<mat-list-option>
|
||||||
<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>
|
<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>
|
}
|
||||||
|
}
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div style="position: relative;" *ngIf="usePaginator && selectedIndex > 0">
|
@if (usePaginator && selectedIndex > 0) {
|
||||||
|
<div style="position: relative;">
|
||||||
<mat-paginator class="paginator" #paginator (page)="pageChangeEvent($event)" [length]="file_count"
|
<mat-paginator class="paginator" #paginator (page)="pageChangeEvent($event)" [length]="file_count"
|
||||||
[pageSize]="pageSize"
|
[pageSize]="pageSize"
|
||||||
[pageSizeOptions]="[5, 10, 25, 100, this.paged_data && this.paged_data.length > 100 ? this.paged_data.length : 250]">
|
[pageSizeOptions]="[5, 10, 25, 100, this.paged_data && this.paged_data.length > 100 ? this.paged_data.length : 250]">
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { distinctUntilChanged } from 'rxjs/operators';
|
|||||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { MatChipListboxChange } from '@angular/material/chips';
|
import { MatChipListboxChange } from '@angular/material/chips';
|
||||||
import { MatSelectionListChange } from '@angular/material/list';
|
import { MatSelectionListChange } from '@angular/material/list';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-recent-videos',
|
selector: 'app-recent-videos',
|
||||||
@@ -380,8 +381,8 @@ export class RecentVideosComponent implements OnInit {
|
|||||||
|
|
||||||
fileSelectionChanged(event: MatSelectionListChange): void {
|
fileSelectionChanged(event: MatSelectionListChange): void {
|
||||||
// TODO: make sure below line is possible (_selected is private)
|
// TODO: make sure below line is possible (_selected is private)
|
||||||
const adding = event.option['_selected'];
|
const adding = event.options['_selected'];
|
||||||
const value = event.option.value;
|
const value = adding.value;
|
||||||
if (adding) {
|
if (adding) {
|
||||||
this.selected_data.push(value.uid);
|
this.selected_data.push(value.uid);
|
||||||
this.selected_data_objs.push(value);
|
this.selected_data_objs.push(value);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<span class="text" [ngStyle]="{'-webkit-line-clamp': !see_more_active ? line_limit : null}" [innerHTML]="text | linkify"></span>
|
<span class="text" [ngStyle]="{'-webkit-line-clamp': !see_more_active ? line_limit : null}" [innerHTML]="text | linkify"></span>
|
||||||
<span>
|
<span>
|
||||||
<a [routerLink]="[]" (click)="toggleSeeMore()">
|
<a [routerLink]="[]" (click)="toggleSeeMore()">
|
||||||
<ng-container *ngIf="!see_more_active" i18n="See more">
|
@if (!see_more_active) {
|
||||||
|
<ng-container i18n="See more">
|
||||||
See more.
|
See more.
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="see_more_active" i18n="See less">
|
} @else {
|
||||||
|
<ng-container i18n="See less">
|
||||||
See less.
|
See less.
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
@@ -1 +1,3 @@
|
|||||||
<button *ngIf="show_skip_ad_button" (click)="skipAdButtonClicked()" mat-flat-button><ng-container i18n="Skip ad button">Skip ad</ng-container></button>
|
@if (show_skip_ad_button) {
|
||||||
|
<button (click)="skipAdButtonClicked()" mat-flat-button><ng-container i18n="Skip ad button">Skip ad</ng-container></button>
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
<div style="display: inline-block;">
|
<div style="display: inline-block;">
|
||||||
<mat-form-field appearance="outline" style="width: 165px;">
|
<mat-form-field appearance="outline" style="width: 165px;">
|
||||||
<mat-select [(ngModel)]="this.sortProperty" (selectionChange)="emitSortOptionChanged()">
|
<mat-select [(ngModel)]="this.sortProperty" (selectionChange)="emitSortOptionChanged()">
|
||||||
<mat-option *ngFor="let sortOption of sortProperties | keyvalue" [value]="sortOption.key">
|
@for (sortOption of sortProperties | keyvalue; track sortOption) {
|
||||||
|
<mat-option [value]="sortOption.key">
|
||||||
{{sortOption['value']['label']}}
|
{{sortOption['value']['label']}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<h4 mat-dialog-title><ng-container i18n="Task settings">Task settings - {{task.title}}</ng-container></h4>
|
<h4 mat-dialog-title><ng-container i18n="Task settings">Task settings - {{task.title}}</ng-container></h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div *ngIf="task_key === 'delete_old_files'">
|
@if (task_key === 'delete_old_files') {
|
||||||
|
<div>
|
||||||
<mat-form-field color="accent">
|
<mat-form-field color="accent">
|
||||||
<mat-label i18n="Delete files older than">Delete files older than</mat-label>
|
<mat-label i18n="Delete files older than">Delete files older than</mat-label>
|
||||||
<input [(ngModel)]="new_options['threshold_days']" matInput onlyNumber required>
|
<input [(ngModel)]="new_options['threshold_days']" matInput onlyNumber required>
|
||||||
@@ -14,16 +14,18 @@
|
|||||||
<mat-checkbox [disabled]="new_options['blacklist_files']" [(ngModel)]="new_options['blacklist_subscription_files']" i18n="Blacklist deleted subscription files" placeholder="Archive mode must be enabled" placeholder-i18n>Blacklist deleted subscription files</mat-checkbox>
|
<mat-checkbox [disabled]="new_options['blacklist_files']" [(ngModel)]="new_options['blacklist_subscription_files']" i18n="Blacklist deleted subscription files" placeholder="Archive mode must be enabled" placeholder-i18n>Blacklist deleted subscription files</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div>
|
<div>
|
||||||
<mat-checkbox [(ngModel)]="new_options['auto_confirm']" i18n="Do not ask for confirmation">Do not ask for confirmation</mat-checkbox>
|
<mat-checkbox [(ngModel)]="new_options['auto_confirm']" i18n="Do not ask for confirmation">Do not ask for confirmation</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close>
|
<button mat-button mat-dialog-close>
|
||||||
<ng-container *ngIf="optionsChanged()" i18n="Task settings cancel button">Cancel</ng-container>
|
@if (optionsChanged()) {
|
||||||
<ng-container *ngIf="!optionsChanged()" i18n="Task settings close button">Close</ng-container>
|
<ng-container i18n="Task settings cancel button">Cancel</ng-container>
|
||||||
|
} @else {
|
||||||
|
<ng-container i18n="Task settings close button">Close</ng-container>
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
<button mat-button [disabled]="!optionsChanged()" (click)="saveSettings()"><ng-container i18n="Save button">Save</ng-container></button>
|
<button mat-button [disabled]="!optionsChanged()" (click)="saveSettings()"><ng-container i18n="Save button">Save</ng-container></button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -10,64 +10,74 @@
|
|||||||
</span>
|
</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Last Ran Column -->
|
<!-- Last Ran Column -->
|
||||||
<ng-container matColumnDef="last_ran">
|
<ng-container matColumnDef="last_ran">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Last ran">Last ran</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Last ran">Last ran</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element">
|
||||||
<ng-container *ngIf="element.last_ran">{{element.last_ran*1000 | date: 'short'}}</ng-container>
|
@if (element.last_ran) {
|
||||||
<ng-container i18n="N/A" *ngIf="!element.last_ran">N/A</ng-container>
|
{{element.last_ran*1000 | date: 'short'}}
|
||||||
|
} @else {
|
||||||
|
<ng-container i18n="N/A">N/A</ng-container>
|
||||||
|
}
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Last Confirmed Column -->
|
<!-- Last Confirmed Column -->
|
||||||
<ng-container matColumnDef="last_confirmed">
|
<ng-container matColumnDef="last_confirmed">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Last confirmed">Last confirmed</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Last confirmed">Last confirmed</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element">
|
||||||
<ng-container *ngIf="element.last_confirmed">{{element.last_confirmed*1000 | date: 'short'}}</ng-container>
|
@if (element.last_confirmed) {
|
||||||
<ng-container i18n="N/A" *ngIf="!element.last_confirmed">N/A</ng-container>
|
{{element.last_confirmed*1000 | date: 'short'}}
|
||||||
|
} @else {
|
||||||
|
<ng-container i18n="N/A">N/A</ng-container>
|
||||||
|
}
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Status Column -->
|
<!-- Status Column -->
|
||||||
<ng-container matColumnDef="status">
|
<ng-container matColumnDef="status">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Status">Status</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Status">Status</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element">
|
||||||
<span *ngIf="element.running || element.confirming"><mat-spinner matTooltip="Busy" i18n-matTooltip="Busy" [diameter]="25"></mat-spinner></span>
|
@if (element.running || element.confirming) {
|
||||||
<span *ngIf="!(element.running || element.confirming) && element.schedule" style="display: flex">
|
<span><mat-spinner matTooltip="Busy" i18n-matTooltip="Busy" [diameter]="25"></mat-spinner></span>
|
||||||
|
} @else if (element.schedule) {
|
||||||
|
<span style="display: flex">
|
||||||
<ng-container i18n="Scheduled">Scheduled for</ng-container>
|
<ng-container i18n="Scheduled">Scheduled for</ng-container>
|
||||||
{{element.next_invocation | date: 'short'}}<mat-icon style="font-size: 16px; display: inline-flex; align-items: center; padding-left: 5px; padding-bottom: 6px;" *ngIf="element.schedule.type === 'recurring'">repeat</mat-icon>
|
{{element.next_invocation | date: 'short'}}
|
||||||
|
@if (element.schedule.type === 'recurring') {
|
||||||
|
<mat-icon style="font-size: 16px; display: inline-flex; align-items: center; padding-left: 5px; padding-bottom: 6px;">repeat</mat-icon>
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="!(element.running || element.confirming) && !element.schedule">
|
} @else {
|
||||||
|
<span>
|
||||||
<ng-container i18n="Not scheduled">Not scheduled</ng-container>
|
<ng-container i18n="Not scheduled">Not scheduled</ng-container>
|
||||||
</span>
|
</span>
|
||||||
|
}
|
||||||
</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> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-start">
|
<div class="row justify-content-start">
|
||||||
<div *ngIf="element.data?.uids?.length > 0 || (!element.data?.uids && element.data)" class="col-12 mt-2" style="display: flex; justify-content: center;">
|
@if (element.data?.uids?.length > 0 || (!element.data?.uids && element.data)) {
|
||||||
|
<div class="col-12 mt-2" style="display: flex; justify-content: center;">
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<button (click)="confirmTask(element.key)" [disabled]="element.running || element.confirming" mat-stroked-button>
|
<button (click)="confirmTask(element.key)" [disabled]="element.running || element.confirming" mat-stroked-button>
|
||||||
<ng-container *ngIf="element.key == 'missing_files_check'">
|
@switch(element.key) {
|
||||||
|
@case ('missing_files_check') {
|
||||||
<ng-container i18n="Clear missing files from DB">Clear missing files from DB:</ng-container>{{element.data.uids.length}}
|
<ng-container i18n="Clear missing files from DB">Clear missing files from DB:</ng-container>{{element.data.uids.length}}
|
||||||
</ng-container>
|
} @case ('duplicate_files_check') {
|
||||||
<ng-container *ngIf="element.key == 'duplicate_files_check'">
|
|
||||||
<ng-container i18n="Clear duplicate files from DB">Clear duplicate files from DB:</ng-container> {{element.data.uids.length}}
|
<ng-container i18n="Clear duplicate files from DB">Clear duplicate files from DB:</ng-container> {{element.data.uids.length}}
|
||||||
</ng-container>
|
} @case ('youtubedl_update_check') {
|
||||||
<ng-container *ngIf="element.key == 'youtubedl_update_check'">
|
|
||||||
<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>
|
} @case ('delete_old_files') {
|
||||||
<ng-container *ngIf="element.key == 'delete_old_files'">
|
|
||||||
<ng-container i18n="Delete old files">Delete old files:</ng-container> {{element.data.files_to_remove.length}}
|
<ng-container i18n="Delete old files">Delete old files:</ng-container> {{element.data.files_to_remove.length}}
|
||||||
</ng-container>
|
}
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<button (click)="runTask(element.key)" [disabled]="element.running || element.confirming" mat-icon-button matTooltip="Run" i18n-matTooltip="Run"><mat-icon>play_arrow</mat-icon></button>
|
<button (click)="runTask(element.key)" [disabled]="element.running || element.confirming" mat-icon-button matTooltip="Run" i18n-matTooltip="Run"><mat-icon>play_arrow</mat-icon></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,28 +87,28 @@
|
|||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<button (click)="openTaskSettings(element)" mat-icon-button matTooltip="Settings" i18n-matTooltip="Settings"><mat-icon>settings</mat-icon></button>
|
<button (click)="openTaskSettings(element)" mat-icon-button matTooltip="Settings" i18n-matTooltip="Settings"><mat-icon>settings</mat-icon></button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="element.error" class="col-3">
|
@if (element.error) {
|
||||||
|
<div class="col-3">
|
||||||
<button (click)="showError(element)" mat-icon-button matTooltip="Show error" i18n-matTooltip="Show error"><mat-icon>warning</mat-icon></button>
|
<button (click)="showError(element)" mat-icon-button matTooltip="Show error" i18n-matTooltip="Show error"><mat-icon>warning</mat-icon></button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
|
|
||||||
<mat-paginator [pageSizeOptions]="[10, 20]"
|
<mat-paginator [pageSizeOptions]="[10, 20]"
|
||||||
showFirstLastButtons
|
showFirstLastButtons
|
||||||
aria-label="Select page of tasks">
|
aria-label="Select page of tasks">
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button style="margin-top: 10px; margin-left: 5px;" mat-stroked-button (click)="openRestoreDBBackupDialog()" i18n="Restore DB from backup button">Restore DB from backup</button>
|
<button style="margin-top: 10px; margin-left: 5px;" mat-stroked-button (click)="openRestoreDBBackupDialog()" i18n="Restore DB from backup button">Restore DB from backup</button>
|
||||||
<button style="margin-top: 10px; margin-left: 5px;" mat-stroked-button (click)="resetTasks()" color="warn" i18n="Reset tasks button">Reset tasks</button>
|
<button style="margin-top: 10px; margin-left: 5px;" mat-stroked-button (click)="resetTasks()" color="warn" i18n="Reset tasks button">Reset tasks</button>
|
||||||
</div>
|
</div>
|
||||||
|
@if ((!tasks || tasks.length === 0) && tasks_retrieved) {
|
||||||
<div *ngIf="(!tasks || tasks.length === 0) && tasks_retrieved">
|
<div>
|
||||||
<h4 style="text-align: center; margin-top: 10px;" i18n="No tasks label">No tasks available!</h4>
|
<h4 style="text-align: center; margin-top: 10px;" i18n="No tasks label">No tasks available!</h4>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
<div class="chat-container" #scrollContainer *ngIf="visible_chat">
|
@if (visible_chat) {
|
||||||
|
<div class="chat-container" #scrollContainer>
|
||||||
<div style="width: 250px; text-align: center;"><strong>Twitch Chat</strong></div>
|
<div style="width: 250px; text-align: center;"><strong>Twitch Chat</strong></div>
|
||||||
<div #chat style="max-width: 250px" *ngFor="let chat of visible_chat; let last = last">
|
@for (chat of visible_chat; track chat; let last = $last) {
|
||||||
|
<div #chat style="max-width: 250px">
|
||||||
{{chat.timestamp_str}} - <strong [style.color]="chat.user_color ? chat.user_color : null">{{chat.name}}</strong>: {{chat.message}}
|
{{chat.timestamp_str}} - <strong [style.color]="chat.user_color ? chat.user_color : null">{{chat.name}}</strong>: {{chat.message}}
|
||||||
{{last ? scrollToBottom() : ''}}
|
{{last ? scrollToBottom() : ''}}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<ng-container *ngIf="chat_response_received && !full_chat">
|
@if (chat_response_received && !full_chat) {
|
||||||
<button [disabled]="downloading_chat" (click)="downloadTwitchChat()" class="download-button" mat-raised-button color="accent"><ng-container i18n="Download Twitch Chat button">Download Twitch Chat</ng-container></button>
|
<button [disabled]="downloading_chat" (click)="downloadTwitchChat()" class="download-button" mat-raised-button color="accent"><ng-container i18n="Download Twitch Chat button">Download Twitch Chat</ng-container></button>
|
||||||
<mat-spinner *ngIf="downloading_chat" class="downloading-spinner" [diameter]="30"></mat-spinner>
|
@if (downloading_chat) {
|
||||||
</ng-container>
|
<mat-spinner class="downloading-spinner" [diameter]="30"></mat-spinner>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +1,103 @@
|
|||||||
<div (mouseenter)="onMouseOver()" (mouseleave)="onMouseOut()" (contextmenu)="onRightClick($event)" style="position: relative; width: fit-content;">
|
<div (mouseenter)="onMouseOver()" (mouseleave)="onMouseOut()" (contextmenu)="onRightClick($event)" style="position: relative; width: fit-content;">
|
||||||
<div *ngIf="!loading" class="download-time">
|
@if (!loading) {
|
||||||
|
<div class="download-time">
|
||||||
<mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
|
<mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
|
||||||
|
|
||||||
<ng-container i18n="Auto-generated label" *ngIf="file_obj.auto">Auto-generated</ng-container>
|
@if (file_obj.auto) {
|
||||||
<ng-container *ngIf="!file_obj.auto">{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</ng-container>
|
<ng-container i18n="Auto-generated label">Auto-generated</ng-container>
|
||||||
|
}
|
||||||
|
@else {
|
||||||
|
{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}
|
||||||
|
}
|
||||||
</div>
|
</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>
|
}
|
||||||
|
@else {
|
||||||
|
<div 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"
|
||||||
[style.top]="contextMenuPosition.y"
|
[style.top]="contextMenuPosition.y"
|
||||||
[matMenuTriggerFor]="context_menu">
|
[matMenuTriggerFor]="context_menu">
|
||||||
</div>
|
</div>
|
||||||
<button *ngIf="!file_obj || !file_obj.auto" [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
@if (!file_obj || !file_obj.auto) {
|
||||||
|
<button [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||||
|
}
|
||||||
<mat-menu #context_menu>
|
<mat-menu #context_menu>
|
||||||
<ng-container *ngIf="!loading">
|
@if (!loading) {
|
||||||
<button (click)="navigateToFile($event)" mat-menu-item><mat-icon>open_in_browser</mat-icon><ng-container i18n="Open file button">Open file</ng-container></button>
|
<button (click)="navigateToFile($event)" mat-menu-item><mat-icon>open_in_browser</mat-icon><ng-container i18n="Open file button">Open file</ng-container></button>
|
||||||
<button (click)="navigateToFile({ctrlKey: true})" mat-menu-item><mat-icon>open_in_new</mat-icon><ng-container i18n="Open file in new tab">Open file in new tab</ng-container></button>
|
<button (click)="navigateToFile({ctrlKey: true})" mat-menu-item><mat-icon>open_in_new</mat-icon><ng-container i18n="Open file in new tab">Open file in new tab</ng-container></button>
|
||||||
</ng-container>
|
}
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
<mat-menu #action_menu="matMenu">
|
<mat-menu #action_menu="matMenu">
|
||||||
<ng-container *ngIf="!is_playlist && !loading">
|
@if (!is_playlist && !loading) {
|
||||||
<button (click)="emitToggleFavorite()" mat-menu-item>
|
<button (click)="emitToggleFavorite()" mat-menu-item>
|
||||||
<mat-icon>{{file_obj.favorite ? 'favorite_filled' : 'favorite_outline'}}</mat-icon>
|
<mat-icon>{{file_obj.favorite ? 'favorite_filled' : 'favorite_outline'}}</mat-icon>
|
||||||
<ng-container *ngIf="!file_obj.favorite" i18n="Favorite button">Favorite</ng-container>
|
@if (!file_obj.favorite) {
|
||||||
<ng-container *ngIf="file_obj.favorite" i18n="Unfavorite button">Unfavorite</ng-container>
|
<ng-container i18n="Favorite button">Favorite</ng-container>
|
||||||
|
}
|
||||||
|
@else {
|
||||||
|
<ng-container i18n="Unfavorite button">Unfavorite</ng-container>
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
||||||
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon> <ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
|
@if (file_obj.sub_id) {
|
||||||
|
<button (click)="navigateToSubscription()" mat-menu-item><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon> <ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
|
||||||
|
}
|
||||||
<button [disabled]="!availablePlaylists || availablePlaylists.length === 0" [matMenuTriggerFor]="addtoplaylist" mat-menu-item><mat-icon>playlist_add</mat-icon> <ng-container i18n="Add to playlist menu item">Add to playlist</ng-container></button>
|
<button [disabled]="!availablePlaylists || availablePlaylists.length === 0" [matMenuTriggerFor]="addtoplaylist" mat-menu-item><mat-icon>playlist_add</mat-icon> <ng-container i18n="Add to playlist menu item">Add to playlist</ng-container></button>
|
||||||
<mat-menu #addtoplaylist="matMenu">
|
<mat-menu #addtoplaylist="matMenu">
|
||||||
<ng-container *ngFor="let playlist of availablePlaylists">
|
@for (playlist of availablePlaylists; track playlist) {
|
||||||
<button *ngIf="(playlist.type === 'audio') === file_obj.isAudio" [disabled]="playlist.uids?.includes(file_obj.uid)" (click)="emitAddFileToPlaylist(playlist.id)" mat-menu-item>{{playlist.name}}</button>
|
@if ((playlist.type === 'audio') === file_obj.isAudio) {
|
||||||
</ng-container>
|
<button [disabled]="playlist.uids?.includes(file_obj.uid)" (click)="emitAddFileToPlaylist(playlist.id)" mat-menu-item>{{playlist.name}}</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container></button>
|
@if (file_obj.sub_id) {
|
||||||
<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 (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>
|
||||||
<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>
|
} @else {
|
||||||
</ng-container>
|
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
|
||||||
<ng-container *ngIf="is_playlist && !loading">
|
}
|
||||||
|
@if (file_obj.sub_id || use_youtubedl_archive) {
|
||||||
|
<button (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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (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>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
|
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
|
||||||
</ng-container>
|
} @else if (loading) {
|
||||||
<ng-container *ngIf="loading">
|
|
||||||
<button mat-menu-item>Placeholder</button>
|
<button mat-menu-item>Placeholder</button>
|
||||||
</ng-container>
|
}
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
|
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
|
||||||
<div style="padding:5px">
|
<div style="padding:5px">
|
||||||
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
|
@if (!loading && file_obj.thumbnailURL) {
|
||||||
|
<div class="img-div">
|
||||||
<div [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" style="position: relative">
|
<div [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" style="position: relative">
|
||||||
<img *ngIf="!hide_image || is_playlist || (file_obj.type === 'audio' || file_obj.isAudio)" [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailPath ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
|
@if (!hide_image || is_playlist || (file_obj.type === 'audio' || file_obj.isAudio)) {
|
||||||
<video *ngIf="elevated && !is_playlist && !(file_obj.type === 'audio' || file_obj.isAudio)" autoplay loop muted [muted]="true" [ngClass]="{'video-small': card_size === 'small', 'video': card_size === 'medium', 'video-large': card_size === 'large'}" [src]="streamURL">
|
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailPath ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
|
||||||
|
}
|
||||||
|
@if (elevated && !is_playlist && !(file_obj.type === 'audio' || file_obj.isAudio)) {
|
||||||
|
<video autoplay loop muted [muted]="true" [ngClass]="{'video-small': card_size === 'small', 'video': card_size === 'medium', 'video-large': card_size === 'large'}" [src]="streamURL">
|
||||||
</video>
|
</video>
|
||||||
|
}
|
||||||
<div class="duration-time">
|
<div class="duration-time">
|
||||||
{{file_length}}
|
{{file_length}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div *ngIf="loading" class="img-div">
|
@if (loading) {
|
||||||
|
<div class="img-div">
|
||||||
<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>
|
<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>
|
||||||
|
} @else {
|
||||||
<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 [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 [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>
|
}
|
||||||
|
@if (loading) {
|
||||||
|
<span 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>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<h4 mat-dialog-title *ngIf="create_mode" ><ng-container i18n="Create a playlist dialog title">Create a playlist</ng-container></h4>
|
@if (create_mode) {
|
||||||
<h4 mat-dialog-title *ngIf="!create_mode"><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
|
<h4 mat-dialog-title ><ng-container i18n="Create a playlist dialog title">Create a playlist</ng-container></h4>
|
||||||
|
} @else {
|
||||||
|
<h4 mat-dialog-title><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
|
||||||
|
}
|
||||||
<mat-dialog-content style="max-height: 85vh;">
|
<mat-dialog-content style="max-height: 85vh;">
|
||||||
<form>
|
<form>
|
||||||
<div *ngIf="create_mode || playlist">
|
@if (create_mode || playlist) {
|
||||||
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field color="accent">
|
<mat-form-field color="accent">
|
||||||
<mat-label i18n="Playlist name">Name</mat-label>
|
<mat-label i18n="Playlist name">Name</mat-label>
|
||||||
@@ -12,17 +15,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<app-recent-videos [selectMode]="true" [defaultSelected]="preselected_files" [customHeader]="'Select files'" (fileSelectionEmitter)="fileSelectionChanged($event)" [selectedIndex]="create_mode ? 1 : 0"></app-recent-videos>
|
<app-recent-videos [selectMode]="true" [defaultSelected]="preselected_files" [customHeader]="'Select files'" (fileSelectionEmitter)="fileSelectionChanged($event)" [selectedIndex]="create_mode ? 1 : 0"></app-recent-videos>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</form>
|
</form>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button *ngIf="create_mode" (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-button>
|
@if (create_mode) {
|
||||||
|
<button (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-button>
|
||||||
<ng-container i18n="Create button">Create</ng-container>
|
<ng-container i18n="Create button">Create</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="!create_mode" (click)="updatePlaylist()" [disabled]="!name || !playlistChanged()" color="primary" style="float: right" mat-button>
|
} @else {
|
||||||
|
<button (click)="updatePlaylist()" [disabled]="!name || !playlistChanged()" color="primary" style="float: right" mat-button>
|
||||||
<ng-container i18n="Save button">Save</ng-container>
|
<ng-container i18n="Save button">Save</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<div *ngIf="create_in_progress" style="margin-left: 10px"><mat-spinner [diameter]="25"></mat-spinner></div>
|
}
|
||||||
|
@if (create_in_progress) {
|
||||||
|
<div style="margin-left: 10px"><mat-spinner [diameter]="25"></mat-spinner></div>
|
||||||
|
}
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
|||||||
@@ -17,17 +17,25 @@
|
|||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<h5 style="margin-top: 10px;">Installation details:</h5>
|
<h5 style="margin-top: 10px;">Installation details:</h5>
|
||||||
<p>
|
<p>
|
||||||
<ng-container i18n="Version label">Installed version:</ng-container> {{current_version_tag}} - <span style="display: inline-block" *ngIf="checking_for_updates"><mat-spinner class="version-spinner" [diameter]="22"></mat-spinner> <ng-container i18n="Checking for updates text">Checking for updates...</ng-container></span>
|
<ng-container i18n="Version label">Installed version:</ng-container> {{current_version_tag}} -
|
||||||
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon> <ng-container *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag"><a [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container></ng-container>
|
@if (checking_for_updates) {
|
||||||
<span *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] === current_version_tag">You are up to date.</span>
|
<span style="display: inline-block"><mat-spinner class="version-spinner" [diameter]="22"></mat-spinner> <ng-container i18n="Checking for updates text">Checking for updates...</ng-container></span>
|
||||||
|
} @else {
|
||||||
|
<mat-icon class="version-checked-icon">done</mat-icon>
|
||||||
|
@if (latestGithubRelease['tag_name'] !== current_version_tag) {
|
||||||
|
<a [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container>
|
||||||
|
} @else {
|
||||||
|
<span>You are up to date.</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<ng-container i18n="Installation type">Installation type:</ng-container> {{postsService.version_info.type}}
|
<ng-container i18n="Installation type">Installation type:</ng-container> {{postsService.version_info.type}}
|
||||||
<br>
|
<br>
|
||||||
<ng-container *ngIf="postsService.version_info.type === 'docker'">
|
@if (postsService.version_info.type === 'docker') {
|
||||||
<ng-container i18n="Docker tag">Docker tag:</ng-container> {{postsService.version_info.tag}}
|
<ng-container i18n="Docker tag">Docker tag:</ng-container> {{postsService.version_info.tag}}
|
||||||
<br>
|
<br>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container i18n="Commit hash">Commit hash:</ng-container> {{postsService.version_info.commit}}
|
<ng-container i18n="Commit hash">Commit hash:</ng-container> {{postsService.version_info.commit}}
|
||||||
<br>
|
<br>
|
||||||
<ng-container i18n="Build date">Build date:</ng-container> {{postsService.version_info.date}}
|
<ng-container i18n="Build date">Build date:</ng-container> {{postsService.version_info.date}}
|
||||||
@@ -37,7 +45,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<h4 i18n="Modify args title" mat-dialog-title>Modify youtube-dl args</h4>
|
<h4 i18n="Modify args title" mat-dialog-title>Modify youtube-dl args</h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -10,23 +9,28 @@
|
|||||||
<mat-chip-grid class="example-chip" #chipList aria-label="Args array" cdkDropList cdkDropListDisabled
|
<mat-chip-grid class="example-chip" #chipList aria-label="Args array" cdkDropList cdkDropListDisabled
|
||||||
cdkDropListOrientation="horizontal"
|
cdkDropListOrientation="horizontal"
|
||||||
(cdkDropListDropped)="drop($event)">
|
(cdkDropListDropped)="drop($event)">
|
||||||
<mat-chip-row [matTooltip]="argsByKey[arg] ? argsByKey[arg]['description'] : null" *ngFor="let arg of args_array; let i = index;" [removable]="removable" (removed)="remove(i)" cdkDrag>
|
@for (arg of args_array; track arg; let i = $index) {
|
||||||
|
<mat-chip-row [matTooltip]="argsByKey[arg] ? argsByKey[arg]['description'] : null" [removable]="removable" (removed)="remove(i)" cdkDrag>
|
||||||
{{arg}}
|
{{arg}}
|
||||||
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
|
@if (removable) {
|
||||||
|
<mat-icon matChipRemove>cancel</mat-icon>
|
||||||
|
}
|
||||||
</mat-chip-row>
|
</mat-chip-row>
|
||||||
|
}
|
||||||
</mat-chip-grid>
|
</mat-chip-grid>
|
||||||
<mat-form-field style="width: 100%" color="accent">
|
<mat-form-field style="width: 100%" color="accent">
|
||||||
|
|
||||||
<input #chipper style="width: 100%;" [formControl]="chipCtrl" matInput [matAutocomplete]="autochip" [matChipInputFor]="chipList"
|
<input #chipper style="width: 100%;" [formControl]="chipCtrl" matInput [matAutocomplete]="autochip" [matChipInputFor]="chipList"
|
||||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||||
[matChipInputAddOnBlur]="addOnBlur"
|
[matChipInputAddOnBlur]="addOnBlur"
|
||||||
(matChipInputTokenEnd)="add($event)">
|
(matChipInputTokenEnd)="add($event)">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-autocomplete #autochip="matAutocomplete">
|
<mat-autocomplete #autochip="matAutocomplete">
|
||||||
<mat-option *ngFor="let arg of filteredChipOptions | async" [value]="arg.key">
|
@for (arg of filteredChipOptions | async; track arg) {
|
||||||
|
<mat-option [value]="arg.key">
|
||||||
<span [innerHTML]="arg.key | highlight : chipCtrl.value"></span>
|
<span [innerHTML]="arg.key | highlight : chipCtrl.value"></span>
|
||||||
<button class="info-autocomplete-icon" [matTooltip]="arg.description" mat-icon-button><mat-icon>info</mat-icon></button>
|
<button class="info-autocomplete-icon" [matTooltip]="arg.description" mat-icon-button><mat-icon>info</mat-icon></button>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
|
}
|
||||||
</mat-autocomplete>
|
</mat-autocomplete>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@@ -42,37 +46,38 @@
|
|||||||
<input matInput [matAutocomplete]="auto" [formControl]="stateCtrl">
|
<input matInput [matAutocomplete]="auto" [formControl]="stateCtrl">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-autocomplete #auto="matAutocomplete">
|
<mat-autocomplete #auto="matAutocomplete">
|
||||||
<mat-option *ngFor="let arg of filteredOptions | async" [value]="arg.key">
|
@for (arg of filteredOptions | async; track arg) {
|
||||||
|
<mat-option [value]="arg.key">
|
||||||
<span [innerHTML]="arg.key | highlight : stateCtrl.value"></span>
|
<span [innerHTML]="arg.key | highlight : stateCtrl.value"></span>
|
||||||
<button class="info-autocomplete-icon" [matTooltip]="arg.description" mat-icon-button><mat-icon>info</mat-icon></button>
|
<button class="info-autocomplete-icon" [matTooltip]="arg.description" mat-icon-button><mat-icon>info</mat-icon></button>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
|
}
|
||||||
</mat-autocomplete>
|
</mat-autocomplete>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<mat-menu #argsByCategoryMenu="matMenu">
|
<mat-menu #argsByCategoryMenu="matMenu">
|
||||||
<ng-container *ngFor="let argsInCategory of argsByCategory | keyvalue">
|
@for (argsInCategory of argsByCategory | keyvalue; track argsInCategory) {
|
||||||
<button mat-menu-item [matMenuTriggerFor]="subMenu">{{argsInfo[argsInCategory.key].label}}</button>
|
<button mat-menu-item [matMenuTriggerFor]="subMenu">{{argsInfo[argsInCategory.key].label}}</button>
|
||||||
<mat-menu #subMenu="matMenu">
|
<mat-menu #subMenu="matMenu">
|
||||||
<button mat-menu-item *ngFor="let arg of argsInCategory.value" (click)="setFirstArg(arg.key)"><div style="display: inline-block;">{{arg.key}}</div> <div class="info-menu-icon"><mat-icon [matTooltip]="arg.description">info</mat-icon></div></button>
|
@for (arg of argsInCategory.value; track arg) {
|
||||||
|
<button mat-menu-item (click)="setFirstArg(arg.key)"><div style="display: inline-block;">{{arg.key}}</div> <div class="info-menu-icon"><mat-icon [matTooltip]="arg.description">info</mat-icon></div></button>
|
||||||
|
}
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</ng-container>
|
}
|
||||||
|
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<button style="margin-bottom: 15px" mat-stroked-button [matMenuTriggerFor]="argsByCategoryMenu"><ng-container i18n="Search args by category button">Search by category</ng-container></button>
|
<button style="margin-bottom: 15px" mat-stroked-button [matMenuTriggerFor]="argsByCategoryMenu"><ng-container i18n="Search args by category button">Search by category</ng-container></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<mat-checkbox color="accent" [ngModelOptions]="{standalone: true}" [(ngModel)]="secondArgEnabled"><ng-container i18n="Use arg value checkbox">Use arg value</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [ngModelOptions]="{standalone: true}" [(ngModel)]="secondArgEnabled"><ng-container i18n="Use arg value checkbox">Use arg value</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="secondArgEnabled">
|
@if (secondArgEnabled) {
|
||||||
|
<div>
|
||||||
<mat-form-field style="width: 75%" color="accent">
|
<mat-form-field style="width: 75%" color="accent">
|
||||||
<mat-label i18n="Arg value">Arg value</mat-label>
|
<mat-label i18n="Arg value">Arg value</mat-label>
|
||||||
<input [ngModelOptions]="{standalone: true}" matInput [disabled]="!secondArgEnabled" [(ngModel)]="secondArg">
|
<input [ngModelOptions]="{standalone: true}" matInput [disabled]="!secondArgEnabled" [(ngModel)]="secondArg">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</form>
|
</form>
|
||||||
<div>
|
<div>
|
||||||
<button (click)="addArg()" [disabled]="!canAddArg()" mat-stroked-button color="accent"><ng-container i18n="Search args by category button">Add arg</ng-container></button>
|
<button (click)="addArg()" [disabled]="!canAddArg()" mat-stroked-button color="accent"><ng-container i18n="Search args by category button">Add arg</ng-container></button>
|
||||||
@@ -82,10 +87,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button [mat-dialog-close]="null"><ng-container i18n="Arg modifier cancel button">Cancel</ng-container></button>
|
<button mat-button [mat-dialog-close]="null"><ng-container i18n="Arg modifier cancel button">Cancel</ng-container></button>
|
||||||
<button mat-button color="accent" [mat-dialog-close]="modified_args"><ng-container i18n="Arg modifier modify button">Modify</ng-container></button>
|
<button mat-button color="accent" [mat-dialog-close]="modified_args"><ng-container i18n="Arg modifier modify button">Modify</ng-container></button>
|
||||||
|
|||||||
@@ -2,24 +2,27 @@
|
|||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div style="margin-bottom: 10px;">
|
<div style="margin-bottom: 10px;">
|
||||||
<!-- We can support text dialogs or dialogs where users must select items from a list -->
|
<!-- We can support text dialogs or dialogs where users must select items from a list -->
|
||||||
<ng-container *ngIf="dialogType === 'text'">
|
@if (dialogType === 'text') {
|
||||||
{{dialogText}}
|
{{dialogText}}
|
||||||
</ng-container>
|
} @else if (dialogType === 'selection_list') {
|
||||||
<ng-container *ngIf="dialogType === 'selection_list'">
|
|
||||||
<mat-selection-list [(ngModel)]="selected_items">
|
<mat-selection-list [(ngModel)]="selected_items">
|
||||||
<mat-list-option *ngFor="let item of list" [value]="item.key">
|
@for (item of list; track item) {
|
||||||
|
<mat-list-option [value]="item.key">
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
</mat-list-option>
|
</mat-list-option>
|
||||||
|
}
|
||||||
</mat-selection-list>
|
</mat-selection-list>
|
||||||
</ng-container>
|
}
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||||
<button [disabled]="dialogType === 'selection_list' && selected_items.length === 0" [color]="warnSubmitColor ? 'warn' : 'primary'" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
|
<button [disabled]="dialogType === 'selection_list' && selected_items.length === 0" [color]="warnSubmitColor ? 'warn' : 'primary'" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
|
||||||
<div class="mat-spinner" *ngIf="submitClicked">
|
@if (submitClicked) {
|
||||||
|
<div class="mat-spinner">
|
||||||
<mat-spinner [diameter]="25"></mat-spinner>
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<button style="float: right;" mat-stroked-button mat-dialog-close>
|
<button style="float: right;" mat-stroked-button mat-dialog-close>
|
||||||
{{cancelText}}
|
{{cancelText}}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<h4 mat-dialog-title i18n="Cookies uploader dialog title">Upload new cookies</h4>
|
<h4 mat-dialog-title i18n="Cookies uploader dialog title">Upload new cookies</h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div>
|
<div>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
@@ -22,19 +21,24 @@
|
|||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody class="upload-name-style">
|
<tbody class="upload-name-style">
|
||||||
<tr *ngFor="let item of files; let i=index">
|
@for (item of files; track item; let i = $index) {
|
||||||
|
<tr>
|
||||||
<td style="vertical-align: middle;">
|
<td style="vertical-align: middle;">
|
||||||
<strong>{{ item.relativePath }}</strong>
|
<strong>{{ item.relativePath }}</strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button [disabled]="uploading || uploaded" (click)="uploadFile()" style="float: right" matTooltip="Upload" i18n-matTooltip="Upload" mat-mini-fab><mat-icon>publish</mat-icon><mat-spinner *ngIf="uploading" class="spinner" [diameter]="38"></mat-spinner></button>
|
<button [disabled]="uploading || uploaded" (click)="uploadFile()" style="float: right" matTooltip="Upload" i18n-matTooltip="Upload" mat-mini-fab><mat-icon>publish</mat-icon>
|
||||||
|
@if (uploading) {
|
||||||
|
<mat-spinner class="spinner" [diameter]="38"></mat-spinner>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions><button style="margin-bottom: 5px;" mat-dialog-close mat-stroked-button><ng-container i18n="Close">Close</ng-container></button></mat-dialog-actions>
|
<mat-dialog-actions><button style="margin-bottom: 5px;" mat-dialog-close mat-stroked-button><ng-container i18n="Close">Close</ng-container></button></mat-dialog-actions>
|
||||||
@@ -1,48 +1,47 @@
|
|||||||
<h4 mat-dialog-title><ng-container i18n="Editing category dialog title">Editing category</ng-container> {{category['name']}}</h4>
|
<h4 mat-dialog-title><ng-container i18n="Editing category dialog title">Editing category</ng-container> {{category['name']}}</h4>
|
||||||
|
|
||||||
<mat-dialog-content style="max-height: 50vh">
|
<mat-dialog-content style="max-height: 50vh">
|
||||||
<mat-form-field style="width: 250px; margin-bottom: 5px;">
|
<mat-form-field style="width: 250px; margin-bottom: 5px;">
|
||||||
<mat-label i18n="Category name">Name</mat-label>
|
<mat-label i18n="Category name">Name</mat-label>
|
||||||
<input matInput [(ngModel)]="category['name']" required>
|
<input matInput [(ngModel)]="category['name']" required>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<h6 style="margin-top: 20px;" i18n="Rules">Rules</h6>
|
<h6 style="margin-top: 20px;" i18n="Rules">Rules</h6>
|
||||||
|
|
||||||
<mat-list>
|
<mat-list>
|
||||||
<mat-list-item *ngFor="let rule of category['rules']; let i = index">
|
@for (rule of category['rules']; track rule) {
|
||||||
<mat-form-field [style.visibility]="i === 0 ? 'hidden' : null" class="operator-select">
|
<mat-list-item>
|
||||||
<mat-select [disabled]="i === 0" [(ngModel)]="rule['preceding_operator']">
|
<mat-form-field [style.visibility]="$index === 0 ? 'hidden' : null" class="operator-select">
|
||||||
|
<mat-select [disabled]="$index === 0" [(ngModel)]="rule['preceding_operator']">
|
||||||
<mat-option value="or">OR</mat-option>
|
<mat-option value="or">OR</mat-option>
|
||||||
<mat-option value="and">AND</mat-option>
|
<mat-option value="and">AND</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="property-select">
|
<mat-form-field class="property-select">
|
||||||
<mat-select [(ngModel)]="rule['property']">
|
<mat-select [(ngModel)]="rule['property']">
|
||||||
<mat-option *ngFor="let propertyOption of propertyOptions" [value]="propertyOption.value">{{propertyOption.label}}</mat-option>
|
@for (propertyOption of propertyOptions; track propertyOption) {
|
||||||
|
<mat-option [value]="propertyOption.value">{{propertyOption.label}}</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="comparator-select">
|
<mat-form-field class="comparator-select">
|
||||||
<mat-select [(ngModel)]="rule['comparator']">
|
<mat-select [(ngModel)]="rule['comparator']">
|
||||||
<mat-option *ngFor="let comparatorOption of comparatorOptions" [value]="comparatorOption.value">{{comparatorOption.label}}</mat-option>
|
@for (comparatorOption of comparatorOptions; track comparatorOption) {
|
||||||
|
<mat-option [value]="comparatorOption.value">{{comparatorOption.label}}</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<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>
|
||||||
<span class="rule-buttons">
|
<span class="rule-buttons">
|
||||||
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
|
<button [disabled]="$index === category['rules'].length-1" (click)="swapRules($index, $index+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
|
||||||
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
|
<button [disabled]="$index === 0" (click)="swapRules($index, $index-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
|
||||||
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
|
<button (click)="removeRule($index)" mat-icon-button><mat-icon>cancel</mat-icon></button>
|
||||||
</span>
|
</span>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
|
}
|
||||||
</mat-list>
|
</mat-list>
|
||||||
|
|
||||||
<button style="margin-bottom: 8px;" mat-icon-button (click)="addNewRule()" matTooltip="Add new rule" i18n-matTooltip="Add new rule tooltip"><mat-icon>add</mat-icon></button>
|
<button style="margin-bottom: 8px;" mat-icon-button (click)="addNewRule()" matTooltip="Add new rule" i18n-matTooltip="Add new rule tooltip"><mat-icon>add</mat-icon></button>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<mat-form-field style="width: 250px; margin-top: 10px;">
|
<mat-form-field style="width: 250px; margin-top: 10px;">
|
||||||
<mat-label i18n="Custom file output">Custom file output</mat-label>
|
<mat-label i18n="Custom file output">Custom file output</mat-label>
|
||||||
<input matInput [(ngModel)]="category['custom_output']">
|
<input matInput [(ngModel)]="category['custom_output']">
|
||||||
@@ -53,12 +52,12 @@
|
|||||||
</mat-hint>
|
</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Cancel">Cancel</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Cancel">Cancel</ng-container></button>
|
||||||
|
|
||||||
<button mat-button [disabled]="categoryChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
|
<button mat-button [disabled]="categoryChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
|
||||||
<div class="mat-spinner" *ngIf="updating">
|
@if (updating) {
|
||||||
|
<div class="mat-spinner">
|
||||||
<mat-spinner [diameter]="25"></mat-spinner>
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
<h4 mat-dialog-title><ng-container i18n="Edit subscription dialog title prefix">Editing</ng-container> {{sub.name}} <ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></h4>
|
<h4 mat-dialog-title><ng-container i18n="Edit subscription dialog title prefix">Editing</ng-container> {{sub.name}}
|
||||||
|
@if (sub.paused) {
|
||||||
|
<ng-container i18n="Paused suffix">(Paused)</ng-container>
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -9,19 +12,23 @@
|
|||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-checkbox (change)="downloadAllToggled()" [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
|
<mat-checkbox (change)="downloadAllToggled()" [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12" *ngIf="editor_initialized">
|
@if (editor_initialized) {
|
||||||
|
<div class="col-12">
|
||||||
<ng-container i18n="Download time range prefix">Download videos uploaded in the last</ng-container>
|
<ng-container i18n="Download time range prefix">Download videos uploaded in the last</ng-container>
|
||||||
<mat-form-field color="accent" class="amount-select">
|
<mat-form-field color="accent" class="amount-select">
|
||||||
<input type="number" matInput [(ngModel)]="timerange_amount" (ngModelChange)="timerangeChanged($event, false)" [disabled]="download_all">
|
<input type="number" matInput [(ngModel)]="timerange_amount" (ngModelChange)="timerangeChanged($event, false)" [disabled]="download_all">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="unit-select">
|
<mat-form-field class="unit-select">
|
||||||
<mat-select color="accent" [(ngModel)]="timerange_unit" (ngModelChange)="timerangeChanged($event, true)" [disabled]="download_all">
|
<mat-select color="accent" [(ngModel)]="timerange_unit" (ngModelChange)="timerangeChanged($event, true)" [disabled]="download_all">
|
||||||
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
|
@for (time_unit of time_units; track time_unit) {
|
||||||
|
<mat-option [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
|
||||||
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
|
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div>
|
<div>
|
||||||
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
||||||
@@ -31,7 +38,9 @@
|
|||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label i18n="Max quality">Max quality</mat-label>
|
<mat-label i18n="Max quality">Max quality</mat-label>
|
||||||
<mat-select [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.maxQuality">
|
<mat-select [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.maxQuality">
|
||||||
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
|
@for (available_quality of available_qualities; track available_quality) {
|
||||||
|
<mat-option [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,12 +68,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Subscribe cancel button">Cancel</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Subscribe cancel button">Cancel</ng-container></button>
|
||||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||||
<button mat-button [disabled]="updating || !subChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
|
<button mat-button [disabled]="updating || !subChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
|
||||||
<div class="mat-spinner" *ngIf="updating">
|
@if (updating) {
|
||||||
|
<div class="mat-spinner">
|
||||||
<mat-spinner [diameter]="25"></mat-spinner>
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<h4 mat-dialog-title><ng-container i18n="Generate RSS URL">Generate RSS URL</ng-container></h4>
|
<h4 mat-dialog-title><ng-container i18n="Generate RSS URL">Generate RSS URL</ng-container></h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
@@ -25,7 +24,9 @@
|
|||||||
<mat-label><ng-container i18n="User">User</ng-container></mat-label>
|
<mat-label><ng-container i18n="User">User</ng-container></mat-label>
|
||||||
<mat-select color="accent" [(ngModel)]="userFilter" (selectionChange)="rebuildURL()" [disabled]="!usersList">
|
<mat-select color="accent" [(ngModel)]="userFilter" (selectionChange)="rebuildURL()" [disabled]="!usersList">
|
||||||
<mat-option [value]="''"><ng-container i18n="None">None</ng-container></mat-option>
|
<mat-option [value]="''"><ng-container i18n="None">None</ng-container></mat-option>
|
||||||
<mat-option *ngFor="let user of usersList" [value]="user.uid"><ng-container>{{user.name}}</ng-container></mat-option>
|
@for (user of usersList; track user) {
|
||||||
|
<mat-option [value]="user.uid"><ng-container>{{user.name}}</ng-container></mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +35,9 @@
|
|||||||
<mat-label><ng-container i18n="Subscription">Subscription</ng-container></mat-label>
|
<mat-label><ng-container i18n="Subscription">Subscription</ng-container></mat-label>
|
||||||
<mat-select color="accent" [(ngModel)]="subscriptionFilter" (selectionChange)="rebuildURL()">
|
<mat-select color="accent" [(ngModel)]="subscriptionFilter" (selectionChange)="rebuildURL()">
|
||||||
<mat-option [value]="''"><ng-container i18n="None">None</ng-container></mat-option>
|
<mat-option [value]="''"><ng-container i18n="None">None</ng-container></mat-option>
|
||||||
<mat-option *ngFor="let sub of postsService.subscriptions" [value]="sub.id"><ng-container>{{sub.name}}</ng-container></mat-option>
|
@for (sub of postsService.subscriptions; track sub) {
|
||||||
|
<mat-option [value]="sub.id"><ng-container>{{sub.name}}</ng-container></mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +55,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-form-field style="width: 100%;">
|
<mat-form-field style="width: 100%;">
|
||||||
<mat-label i18n="URL">URL</mat-label>
|
<mat-label i18n="URL">URL</mat-label>
|
||||||
<input readonly [(ngModel)]="url" matInput>
|
<input readonly [(ngModel)]="url" matInput>
|
||||||
@@ -61,7 +63,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<h4 mat-dialog-title><ng-container i18n="Restore DB from backup">Restore DB from backup</ng-container></h4>
|
<h4 mat-dialog-title><ng-container i18n="Restore DB from backup">Restore DB from backup</ng-container></h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<mat-selection-list [multiple]="false" [(ngModel)]="selected_backup">
|
<mat-selection-list [multiple]="false" [(ngModel)]="selected_backup">
|
||||||
<mat-list-option *ngFor="let db_backup of db_backups" [value]="db_backup.name" [matTooltip]="db_backup.name">
|
@for (db_backup of db_backups; track db_backup) {
|
||||||
|
<mat-list-option [value]="db_backup.name" [matTooltip]="db_backup.name">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
@@ -17,13 +17,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-list-option>
|
</mat-list-option>
|
||||||
|
}
|
||||||
</mat-selection-list>
|
</mat-selection-list>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Restore DB cancel button">Cancel</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Restore DB cancel button">Cancel</ng-container></button>
|
||||||
<button mat-button [disabled]="restoring" (click)="restoreClicked()" [disabled]="!selected_backup || selected_backup.length !== 1"><ng-container i18n="Restore button">Restore</ng-container></button>
|
<button mat-button [disabled]="restoring" (click)="restoreClicked()" [disabled]="!selected_backup || selected_backup.length !== 1"><ng-container i18n="Restore button">Restore</ng-container></button>
|
||||||
<div class="mat-spinner" *ngIf="restoring">
|
@if (restoring) {
|
||||||
|
<div class="mat-spinner">
|
||||||
<mat-spinner [diameter]="25"></mat-spinner>
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<h4 mat-dialog-title><ng-container i18n="Create admin account dialog title">Create admin account</ng-container></h4>
|
<h4 mat-dialog-title><ng-container i18n="Create admin account dialog title">Create admin account</ng-container></h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div>
|
<div>
|
||||||
<p i18n="No default admin detected explanation">No default admin account detected. This will create and set the password for an admin account with the user name as 'admin'.</p>
|
<p i18n="No default admin detected explanation">No default admin account detected. This will create and set the password for an admin account with the user name as 'admin'.</p>
|
||||||
@@ -13,8 +12,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button [disabled]="input.length === 0" color="accent" style="margin-bottom: 12px;" (click)="create()" mat-raised-button><ng-container i18n="Create">Create</ng-container></button>
|
<button [disabled]="input.length === 0" color="accent" style="margin-bottom: 12px;" (click)="create()" mat-raised-button><ng-container i18n="Create">Create</ng-container></button>
|
||||||
<div class="spinner-div"><mat-spinner [diameter]="25" *ngIf="creating"></mat-spinner></div>
|
<div class="spinner-div">
|
||||||
|
@if (creating) {
|
||||||
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<h4 mat-dialog-title>
|
<h4 mat-dialog-title>
|
||||||
<ng-container *ngIf="is_playlist" i18n="Share playlist dialog title">Share playlist</ng-container>
|
@if (is_playlist) {
|
||||||
<ng-container *ngIf="!is_playlist" i18n="Share video dialog title">Share file</ng-container>
|
<ng-container i18n="Share playlist dialog title">Share playlist</ng-container>
|
||||||
|
} @else {
|
||||||
|
<ng-container i18n="Share video dialog title">Share file</ng-container>
|
||||||
|
}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
@@ -25,7 +27,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Close button">Close</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Close button">Close</ng-container></button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<h4 mat-dialog-title i18n="Subscribe dialog title">Subscribe to playlist or channel</h4>
|
<h4 mat-dialog-title i18n="Subscribe dialog title">Subscribe to playlist or channel</h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -32,9 +31,11 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="unit-select">
|
<mat-form-field class="unit-select">
|
||||||
<mat-select color="accent" [(ngModel)]="timerange_unit" [disabled]="download_all">
|
<mat-select color="accent" [(ngModel)]="timerange_unit" [disabled]="download_all">
|
||||||
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
|
@for (time_unit of time_units; track time_unit) {
|
||||||
|
<mat-option [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
|
||||||
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
|
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +44,9 @@
|
|||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label i18n="Max quality">Max quality</mat-label>
|
<mat-label i18n="Max quality">Max quality</mat-label>
|
||||||
<mat-select [disabled]="audioOnlyMode" [(ngModel)]="maxQuality">
|
<mat-select [disabled]="audioOnlyMode" [(ngModel)]="maxQuality">
|
||||||
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
|
@for (available_quality of available_qualities; track available_quality) {
|
||||||
|
<mat-option [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,12 +79,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Subscribe cancel button">Cancel</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Subscribe cancel button">Cancel</ng-container></button>
|
||||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||||
<button mat-button [disabled]="!url" type="submit" (click)="subscribeClicked()"><ng-container i18n="Subscribe button">Subscribe</ng-container></button>
|
<button mat-button [disabled]="!url" type="submit" (click)="subscribeClicked()"><ng-container i18n="Subscribe button">Subscribe</ng-container></button>
|
||||||
<div class="mat-spinner" *ngIf="subscribing">
|
@if (subscribing) {
|
||||||
|
<div class="mat-spinner">
|
||||||
<mat-spinner [diameter]="25"></mat-spinner>
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
<h4 mat-dialog-title>{{sub.name}} <ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></h4>
|
<h4 mat-dialog-title>{{sub.name}}
|
||||||
|
@if (sub.paused) {
|
||||||
|
<ng-container i18n="Paused suffix">(Paused)</ng-container>
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<strong><ng-container i18n="Subscription type property">Type:</ng-container> </strong>
|
<strong><ng-container i18n="Subscription type property">Type:</ng-container> </strong>
|
||||||
@@ -13,12 +16,13 @@
|
|||||||
<strong><ng-container i18n="Subscription ID property">ID:</ng-container> </strong>
|
<strong><ng-container i18n="Subscription ID property">ID:</ng-container> </strong>
|
||||||
<span class="info-item-value">{{sub.id}}</span>
|
<span class="info-item-value">{{sub.id}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item" *ngIf="sub.archive">
|
@if (sub.archive) {
|
||||||
|
<div class="info-item">
|
||||||
<strong><ng-container i18n="Subscription ID property">Archive:</ng-container> </strong>
|
<strong><ng-container i18n="Subscription ID property">Archive:</ng-container> </strong>
|
||||||
<span class="info-item-value">{{sub.archive}}</span>
|
<span class="info-item-value">{{sub.archive}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Close subscription info button">Close</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Close subscription info button">Close</ng-container></button>
|
||||||
<button mat-stroked-button (click)="downloadArchive()" color="accent"><ng-container i18n="Export Archive button">Export Archive</ng-container></button>
|
<button mat-stroked-button (click)="downloadArchive()" color="accent"><ng-container i18n="Export Archive button">Export Archive</ng-container></button>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Component, OnInit, Inject } from '@angular/core';
|
|||||||
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component';
|
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component';
|
||||||
|
import { Subscription } from 'api-types';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-subscription-info-dialog',
|
selector: 'app-subscription-info-dialog',
|
||||||
@@ -10,7 +12,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.compone
|
|||||||
})
|
})
|
||||||
export class SubscriptionInfoDialogComponent implements OnInit {
|
export class SubscriptionInfoDialogComponent implements OnInit {
|
||||||
|
|
||||||
sub = null;
|
sub: Subscription = null;
|
||||||
unsubbedEmitter = null;
|
unsubbedEmitter = null;
|
||||||
|
|
||||||
constructor(public dialogRef: MatDialogRef<SubscriptionInfoDialogComponent>,
|
constructor(public dialogRef: MatDialogRef<SubscriptionInfoDialogComponent>,
|
||||||
@@ -41,7 +43,7 @@ export class SubscriptionInfoDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe() {
|
unsubscribe() {
|
||||||
this.postsService.unsubscribe(this.sub, true).subscribe(res => {
|
this.postsService.unsubscribe(this.sub.id, true).subscribe(res => {
|
||||||
this.unsubbedEmitter.emit(true);
|
this.unsubbedEmitter.emit(true);
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
<h4 i18n="Update progress dialog title" mat-dialog-title>Updater</h4>
|
<h4 i18n="Update progress dialog title" mat-dialog-title>Updater</h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div *ngIf="updateStatus">
|
@if (updateStatus) {
|
||||||
|
<div>
|
||||||
<div style="margin-bottom: 8px;">
|
<div style="margin-bottom: 8px;">
|
||||||
<h6 *ngIf="updateStatus['updating']">Update in progress</h6>
|
@if (updateStatus['updating']) {
|
||||||
<h6 *ngIf="!updateStatus['updating'] && updateStatus['error']">Update failed</h6>
|
<h6>Update in progress</h6>
|
||||||
<h6 *ngIf="!updateStatus['updating'] && !updateStatus['error']">Update succeeded!</h6>
|
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||||
|
} @else {
|
||||||
|
@if (updateStatus['error']) {
|
||||||
|
<h6>Update failed</h6>
|
||||||
|
} @else {
|
||||||
|
<h6>Update succeeded!</h6>
|
||||||
|
}
|
||||||
|
<mat-progress-bar mode="determinate" value="100"></mat-progress-bar>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<mat-progress-bar *ngIf="updateStatus['updating']" mode="indeterminate"></mat-progress-bar>
|
@if (updateStatus['details']) {
|
||||||
<mat-progress-bar *ngIf="!updateStatus['updating']" mode="determinate" value="100"></mat-progress-bar>
|
<p style="margin-top: 4px; font-size: 13px;">{{updateStatus['details']}}</p>
|
||||||
<p style="margin-top: 4px; font-size: 13px;" *ngIf="updateStatus['details']">{{updateStatus['details']}}</p>
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Close update progress dialog">Close</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Close update progress dialog">Close</ng-container></button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<h4 mat-dialog-title><ng-container i18n="Update task schedule">Update task schedule</ng-container></h4>
|
<h4 mat-dialog-title><ng-container i18n="Update task schedule">Update task schedule</ng-container></h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -9,7 +8,8 @@
|
|||||||
<div class="col-12 mt-2">
|
<div class="col-12 mt-2">
|
||||||
<mat-checkbox [(ngModel)]="recurring" [disabled]="!enabled"><ng-container i18n="Recurring">Recurring</ng-container></mat-checkbox>
|
<mat-checkbox [(ngModel)]="recurring" [disabled]="!enabled"><ng-container i18n="Recurring">Recurring</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-2" *ngIf="recurring">
|
@if (recurring) {
|
||||||
|
<div class="col-12 mt-2">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-select placeholder="Interval" [(ngModel)]="interval" [disabled]="!enabled">
|
<mat-select placeholder="Interval" [(ngModel)]="interval" [disabled]="!enabled">
|
||||||
<mat-option value="weekly">Weekly</mat-option>
|
<mat-option value="weekly">Weekly</mat-option>
|
||||||
@@ -17,15 +17,8 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!recurring" class="col-12 mt-2">
|
@if (interval === 'weekly') {
|
||||||
<mat-form-field>
|
<div class="col-12 mt-2">
|
||||||
<mat-label i18n="Choose a date">Choose a date</mat-label>
|
|
||||||
<input [(ngModel)]="date" [min]="today" matInput [matDatepicker]="picker" [disabled]="!enabled">
|
|
||||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
|
||||||
<mat-datepicker #picker></mat-datepicker>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="recurring && interval === 'weekly'" class="col-12 mt-2">
|
|
||||||
<mat-button-toggle-group [(ngModel)]="days_of_week" [multiple]="true" [disabled]="!enabled" aria-label="Week day">
|
<mat-button-toggle-group [(ngModel)]="days_of_week" [multiple]="true" [disabled]="!enabled" aria-label="Week day">
|
||||||
<!-- TODO: support translation -->
|
<!-- TODO: support translation -->
|
||||||
<mat-button-toggle [value]="0">M</mat-button-toggle>
|
<mat-button-toggle [value]="0">M</mat-button-toggle>
|
||||||
@@ -37,17 +30,29 @@
|
|||||||
<mat-button-toggle [value]="6">S</mat-button-toggle>
|
<mat-button-toggle [value]="6">S</mat-button-toggle>
|
||||||
</mat-button-toggle-group>
|
</mat-button-toggle-group>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<div class="col-12 mt-2">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label i18n="Choose a date">Choose a date</mat-label>
|
||||||
|
<input [(ngModel)]="date" [min]="today" matInput [matDatepicker]="picker" [disabled]="!enabled">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #picker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="col-12 mt-2">
|
<div class="col-12 mt-2">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>Time</mat-label>
|
<mat-label>Time</mat-label>
|
||||||
<input type="time" matInput [(ngModel)]="time" [disabled]="!enabled">
|
<input type="time" matInput [(ngModel)]="time" [disabled]="!enabled">
|
||||||
<mat-hint *ngIf="Intl?.DateTimeFormat().resolvedOptions().timeZone">{{Intl.DateTimeFormat().resolvedOptions().timeZone}}</mat-hint>
|
@if (Intl?.DateTimeFormat().resolvedOptions().timeZone) {
|
||||||
|
<mat-hint>{{Intl.DateTimeFormat().resolvedOptions().timeZone}}</mat-hint>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Update task schedule cancel button">Cancel</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Update task schedule cancel button">Cancel</ng-container></button>
|
||||||
<button mat-button (click)="updateTaskSchedule()"><ng-container i18n="Update button">Update</ng-container></button>
|
<button mat-button (click)="updateTaskSchedule()"><ng-container i18n="Update button">Update</ng-container></button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<h4 mat-dialog-title i18n="User profile dialog title">Your Profile</h4>
|
<h4 mat-dialog-title i18n="User profile dialog title">Your Profile</h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div *ngIf="postsService.isLoggedIn && postsService.user">
|
@if (postsService.isLoggedIn && postsService.user) {
|
||||||
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<strong><ng-container i18n="Name">Name:</ng-container></strong> {{postsService.user.name}}
|
<strong><ng-container i18n="Name">Name:</ng-container></strong> {{postsService.user.name}}
|
||||||
</div>
|
</div>
|
||||||
@@ -15,14 +15,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<mat-divider style="margin-bottom: 20px"></mat-divider>
|
<mat-divider style="margin-bottom: 20px"></mat-divider>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<mat-form-field color="accent">
|
<mat-form-field color="accent">
|
||||||
<mat-label><ng-container i18n="Language select label">Language</ng-container></mat-label>
|
<mat-label><ng-container i18n="Language select label">Language</ng-container></mat-label>
|
||||||
<mat-select (selectionChange)="localeSelectChanged($event.value)" [(value)]="initialLocale">
|
<mat-select (selectionChange)="localeSelectChanged($event.value)" [(value)]="initialLocale">
|
||||||
<mat-option *ngFor="let locale of supported_locales" [value]="locale">
|
@for (locale of supported_locales; track locale) {
|
||||||
<ng-container *ngIf="all_locales[locale]">
|
<mat-option [value]="locale">
|
||||||
|
@if (all_locales[locale]) {
|
||||||
{{all_locales[locale]['nativeName']}}
|
{{all_locales[locale]['nativeName']}}
|
||||||
</ng-container>
|
}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<br/>
|
<br/>
|
||||||
@@ -53,12 +56,13 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</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 *ngIf="postsService.isLoggedIn" style="position: absolute; right: 0px;" (click)="logoutClicked()" mat-stroked-button color="warn"><ng-container i18n="Logout">Logout</ng-container></button>
|
@if (postsService.isLoggedIn) {
|
||||||
|
<button 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>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
<button [disabled]="!initialized || retrieving_file || !write_access" (click)="toggleFavorite()" mat-icon-button ><mat-icon>{{file.favorite ? 'favorite_filled' : 'favorite_outline'}}</mat-icon></button>
|
<button [disabled]="!initialized || retrieving_file || !write_access" (click)="toggleFavorite()" mat-icon-button ><mat-icon>{{file.favorite ? 'favorite_filled' : 'favorite_outline'}}</mat-icon></button>
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<mat-form-field class="info-field">
|
<mat-form-field class="info-field">
|
||||||
<mat-label i18n="Name">Name</mat-label>
|
<mat-label i18n="Name">Name</mat-label>
|
||||||
@@ -36,17 +35,21 @@
|
|||||||
<mat-label i18n="Thumbnail URL">Thumbnail URL</mat-label>
|
<mat-label i18n="Thumbnail URL">Thumbnail URL</mat-label>
|
||||||
<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">
|
@if (initialized && postsService.categories) {
|
||||||
|
<mat-form-field class="info-field">
|
||||||
<mat-label i18n="Category">Category</mat-label>
|
<mat-label i18n="Category">Category</mat-label>
|
||||||
<mat-select [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
|
<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>
|
||||||
<mat-option *ngFor="let available_category of postsService.categories | keyvalue" [value]="available_category.value">
|
@for (available_category of postsService.categories | keyvalue; track available_category) {
|
||||||
|
<mat-option [value]="available_category.value">
|
||||||
{{available_category.value['name']}}
|
{{available_category.value['name']}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
}
|
||||||
<mat-form-field class="info-field">
|
<mat-form-field class="info-field">
|
||||||
<mat-label i18n="View count">View count</mat-label>
|
<mat-label i18n="View count">View count</mat-label>
|
||||||
<input type="number" [(ngModel)]="new_file.view_count" matInput [disabled]="!editing">
|
<input type="number" [(ngModel)]="new_file.view_count" matInput [disabled]="!editing">
|
||||||
@@ -55,13 +58,13 @@
|
|||||||
<mat-label i18n="Local view count">Local view count</mat-label>
|
<mat-label i18n="Local view count">Local view count</mat-label>
|
||||||
<input type="number" [(ngModel)]="new_file.local_view_count" matInput [disabled]="!editing">
|
<input type="number" [(ngModel)]="new_file.local_view_count" matInput [disabled]="!editing">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-divider style="margin-bottom: 16px;"></mat-divider>
|
<mat-divider style="margin-bottom: 16px;"></mat-divider>
|
||||||
|
@if (!new_file.isAudio) {
|
||||||
<div *ngIf="!new_file.isAudio" class="info-item">
|
<div class="info-item">
|
||||||
<div class="info-item-label"><strong><ng-container i18n="Video resolution property">Resolution:</ng-container> </strong></div>
|
<div class="info-item-label"><strong><ng-container i18n="Video resolution property">Resolution:</ng-container> </strong></div>
|
||||||
<div class="info-item-value">{{new_file.height ? new_file.height + 'p' : 'N/A'}}</div>
|
<div class="info-item-value">{{new_file.height ? new_file.height + 'p' : 'N/A'}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<div class="info-item-label"><strong><ng-container i18n="Video audio bitrate property">Audio bitrate:</ng-container> </strong></div>
|
<div class="info-item-label"><strong><ng-container i18n="Video audio bitrate property">Audio bitrate:</ng-container> </strong></div>
|
||||||
<div class="info-item-value">{{new_file.abr ? new_file.abr + ' Kbps' : 'N/A'}}</div>
|
<div class="info-item-value">{{new_file.abr ? new_file.abr + ' Kbps' : 'N/A'}}</div>
|
||||||
@@ -74,9 +77,7 @@
|
|||||||
<div class="info-item-label"><strong><ng-container i18n="Video path property">Path:</ng-container> </strong></div>
|
<div class="info-item-label"><strong><ng-container i18n="Video path property">Path:</ng-container> </strong></div>
|
||||||
<div class="info-item-value">{{new_file.path ? new_file.path : 'N/A'}}</div>
|
<div class="info-item-value">{{new_file.path ? new_file.path : 'N/A'}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close><ng-container i18n="Close video info button">Close</ng-container></button>
|
<button mat-button mat-dialog-close><ng-container i18n="Close video info button">Close</ng-container></button>
|
||||||
<button mat-button [disabled]="!metadataChanged()" (click)="saveChanges()"><ng-container i18n="Save video info button">Save</ng-container></button>
|
<button mat-button [disabled]="!metadataChanged()" (click)="saveChanges()"><ng-container i18n="Save video info button">Save</ng-container></button>
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
<button mat-button mat-dialog-close>Cancel</button>
|
<button mat-button mat-dialog-close>Cancel</button>
|
||||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||||
<button mat-button [disabled]="!inputText" type="submit" (click)="enterPressed()">{{submitText}}</button>
|
<button mat-button [disabled]="!inputText" type="submit" (click)="enterPressed()">{{submitText}}</button>
|
||||||
<div class="mat-spinner" *ngIf="inputSubmitted">
|
@if (inputSubmitted) {
|
||||||
|
<div class="mat-spinner">
|
||||||
<mat-spinner [diameter]="25"></mat-spinner>
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<!--<button type="button" class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>-->
|
<!--<button type="button" class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>-->
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="allowQualitySelect" class="col-7 col-sm-3">
|
@if (allowQualitySelect) {
|
||||||
|
<div class="col-7 col-sm-3">
|
||||||
<mat-form-field color="accent" style="display: inline-block; width: inherit; min-width: 120px;">
|
<mat-form-field color="accent" style="display: inline-block; width: inherit; min-width: 120px;">
|
||||||
<mat-label>
|
<mat-label>
|
||||||
<ng-container i18n="Quality select label">
|
<ng-container i18n="Quality select label">
|
||||||
@@ -23,30 +24,37 @@
|
|||||||
<mat-option i18n="Best" [value]="''">
|
<mat-option i18n="Best" [value]="''">
|
||||||
Best
|
Best
|
||||||
</mat-option>
|
</mat-option>
|
||||||
<ng-container *ngIf="url && cachedAvailableFormats && cachedAvailableFormats[url]?.formats && !cachedAvailableFormats[url]?.formats_failed">
|
@if (url && cachedAvailableFormats && cachedAvailableFormats[url]?.formats && !cachedAvailableFormats[url]?.formats_failed) {
|
||||||
<ng-container *ngFor="let option of cachedAvailableFormats[url]['formats'][audioOnly ? 'audio' : 'video']">
|
@for (option of cachedAvailableFormats[url]['formats'][audioOnly ? 'audio' : 'video']; track option) {
|
||||||
<mat-option [matTooltip]="option.expected_filesize ? humanFileSize(option.expected_filesize) : null" *ngIf="option.key !== 'best_audio_format'" [value]="option">
|
@if (option.key !== 'best_audio_format') {
|
||||||
|
<mat-option [matTooltip]="option.expected_filesize ? humanFileSize(option.expected_filesize) : null" [value]="option">
|
||||||
{{option.key}}
|
{{option.key}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</ng-container>
|
}
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngIf="url && cachedAvailableFormats && cachedAvailableFormats[url]?.formats_failed">
|
}
|
||||||
<ng-container *ngFor="let option of qualityOptions[audioOnly ? 'audio' : 'video']">
|
@if (url && cachedAvailableFormats && cachedAvailableFormats[url]?.formats_failed) {
|
||||||
|
@for (option of qualityOptions[audioOnly ? 'audio' : 'video']; track option) {
|
||||||
<mat-option [value]="option.value">
|
<mat-option [value]="option.value">
|
||||||
{{option.label}}
|
{{option.label}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</ng-container>
|
}
|
||||||
</ng-container>
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="spinner-div" *ngIf="url !== '' && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']">
|
@if (url !== '' && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']) {
|
||||||
|
<div class="spinner-div">
|
||||||
<mat-spinner [diameter]="25"></mat-spinner>
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@if (results_showing) {
|
||||||
<div class="results-div" *ngIf="results_showing">
|
<div class="results-div">
|
||||||
<span *ngFor="let result of results; let i = index">
|
@for (result of results; track result; let i = $index) {
|
||||||
|
<span>
|
||||||
<mat-card appearance="outlined" class="result-card mat-elevation-z7" [ngClass]="[(i === 0 && results.length > 1) ? 'first-result-card' : '', ((i === results.length-1) && results.length > 1) ? 'last-result-card' : '', (results.length === 1) ? 'only-result-card' : '']">
|
<mat-card appearance="outlined" class="result-card mat-elevation-z7" [ngClass]="[(i === 0 && results.length > 1) ? 'first-result-card' : '', ((i === results.length-1) && results.length > 1) ? 'last-result-card' : '', (results.length === 1) ? 'only-result-card' : '']">
|
||||||
<div class="search-card-title">
|
<div class="search-card-title">
|
||||||
{{result.title}}
|
{{result.title}}
|
||||||
@@ -64,7 +72,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</span>
|
</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</form>
|
</form>
|
||||||
<br/>
|
<br/>
|
||||||
<mat-checkbox [disabled]="autoplay && !!current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px; margin-left: 4px;">
|
<mat-checkbox [disabled]="autoplay && !!current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px; margin-left: 4px;">
|
||||||
@@ -72,12 +82,13 @@
|
|||||||
Only Audio
|
Only Audio
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
<mat-checkbox *ngIf="!forceAutoplay" [disabled]="getURLArray(url).length > 1" (change)="autoplayChanged($event)" [(ngModel)]="autoplay" style="float: right; margin-top: -12px">
|
@if (!forceAutoplay) {
|
||||||
|
<mat-checkbox [disabled]="getURLArray(url).length > 1" (change)="autoplayChanged($event)" [(ngModel)]="autoplay" style="float: right; margin-top: -12px">
|
||||||
<ng-container i18n="Autoplay checkbox">
|
<ng-container i18n="Autoplay checkbox">
|
||||||
Autoplay
|
Autoplay
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
<mat-card-actions>
|
<mat-card-actions>
|
||||||
@@ -86,15 +97,18 @@
|
|||||||
Download
|
Download
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button (click)="cancelDownload()" style="margin-left: 8px; margin-bottom: 8px" *ngIf="!!current_download" mat-stroked-button color="warn">
|
@if (!!current_download) {
|
||||||
|
<button (click)="cancelDownload()" style="margin-left: 8px; margin-bottom: 8px" mat-stroked-button color="warn">
|
||||||
<ng-container i18n="Cancel download button">
|
<ng-container i18n="Cancel download button">
|
||||||
Cancel
|
Cancel
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="allowAdvancedDownload" class="big demo-basic">
|
@if (allowAdvancedDownload) {
|
||||||
|
<div class="big demo-basic">
|
||||||
<form style="margin-left: 20px; margin-right: 20px;">
|
<form style="margin-left: 20px; margin-right: 20px;">
|
||||||
<mat-expansion-panel class="big no-border-radius-top">
|
<mat-expansion-panel class="big no-border-radius-top">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
@@ -104,11 +118,13 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<p *ngIf="this.simulatedOutput">
|
@if (this.simulatedOutput) {
|
||||||
|
<p>
|
||||||
<ng-container i18n="Simulated command label">
|
<ng-container i18n="Simulated command label">
|
||||||
Simulated command:
|
Simulated command:
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<i>{{this.simulatedOutput}}</i></p>
|
<i>{{this.simulatedOutput}}</i></p>
|
||||||
|
}
|
||||||
<div class="container" style="padding-bottom: 20px;">
|
<div class="container" style="padding-bottom: 20px;">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-sm-6">
|
<div class="col-12 col-sm-6">
|
||||||
@@ -148,69 +164,86 @@
|
|||||||
</mat-hint>
|
</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
|
@if (!youtubeAuthDisabledOverride) {
|
||||||
|
<div class="col-12 col-sm-6 mt-3">
|
||||||
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="youtubeAuthEnabledChanged($event)" [(ngModel)]="youtubeAuthEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
|
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="youtubeAuthEnabledChanged($event)" [(ngModel)]="youtubeAuthEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
|
||||||
<ng-container i18n="Use authentication checkbox">
|
<ng-container i18n="Use authentication checkbox">
|
||||||
Use authentication
|
Use authentication
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
<mat-form-field *ngIf="youtubeAuthEnabled" color="accent" class="advanced-input">
|
@if (youtubeAuthEnabled) {
|
||||||
|
<mat-form-field color="accent" class="advanced-input">
|
||||||
<mat-label i18n="Username">Username</mat-label>
|
<mat-label i18n="Username">Username</mat-label>
|
||||||
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()">
|
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
|
<div class="col-12 col-sm-6 mt-3">
|
||||||
<mat-form-field *ngIf="youtubeAuthEnabled" style="margin-top: 40px;" color="accent" class="advanced-input">
|
@if (youtubeAuthEnabled) {
|
||||||
|
<mat-form-field style="margin-top: 40px;" color="accent" class="advanced-input">
|
||||||
<mat-label i18n="Password">Password</mat-label>
|
<mat-label i18n="Password">Password</mat-label>
|
||||||
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()">
|
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div class="col-12 col-sm-6 mt-3">
|
<div class="col-12 col-sm-6 mt-3">
|
||||||
<mat-checkbox color="accent" [disabled]="!!current_download" [(ngModel)]="cropFile" style="z-index: 999" [ngModelOptions]="{standalone: true}">
|
<mat-checkbox color="accent" [disabled]="!!current_download" [(ngModel)]="cropFile" style="z-index: 999" [ngModelOptions]="{standalone: true}">
|
||||||
<ng-container i18n="Crop video checkbox">
|
<ng-container i18n="Crop video checkbox">
|
||||||
Crop file
|
Crop file
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
<mat-form-field *ngIf="cropFile" color="accent" class="advanced-input">
|
@if (cropFile) {
|
||||||
|
<mat-form-field color="accent" class="advanced-input">
|
||||||
<mat-label i18n="Crop from (seconds)">Crop from (seconds)</mat-label>
|
<mat-label i18n="Crop from (seconds)">Crop from (seconds)</mat-label>
|
||||||
<input [(ngModel)]="cropFileStart" type="number" [ngModelOptions]="{standalone: true}" matInput>
|
<input [(ngModel)]="cropFileStart" type="number" [ngModelOptions]="{standalone: true}" matInput>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-sm-6 mt-3">
|
<div class="col-12 col-sm-6 mt-3">
|
||||||
<mat-form-field *ngIf="cropFile" style="margin-top: 40px;" color="accent" class="advanced-input">
|
@if (cropFile) {
|
||||||
|
<mat-form-field style="margin-top: 40px;" color="accent" class="advanced-input">
|
||||||
<mat-label i18n="Crop to (seconds)">Crop to (seconds)</mat-label>
|
<mat-label i18n="Crop to (seconds)">Crop to (seconds)</mat-label>
|
||||||
<input [(ngModel)]="cropFileEnd" type="number" [ngModelOptions]="{standalone: true}" matInput>
|
<input [(ngModel)]="cropFileEnd" type="number" [ngModelOptions]="{standalone: true}" matInput>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<br/>
|
<br/>
|
||||||
<div class="centered big" id="bar_div" *ngIf="current_download && autoplay">
|
@if (current_download && autoplay) {
|
||||||
|
<div class="centered big" id="bar_div">
|
||||||
<div class="margined">
|
<div class="margined">
|
||||||
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
|
@if (current_download.percent_complete && current_download.percent_complete > 1) {
|
||||||
|
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px">
|
||||||
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="+percentDownloaded > 99" class="spinner">
|
} @else {
|
||||||
|
<mat-progress-bar style="border-radius: 5px;" mode="indeterminate"></mat-progress-bar>
|
||||||
|
}
|
||||||
|
@if (+percentDownloaded > 99) {
|
||||||
|
<div class="spinner">
|
||||||
<mat-spinner [diameter]="25"></mat-spinner>
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #indeterminateprogress>
|
}
|
||||||
<mat-progress-bar style="border-radius: 5px;" mode="indeterminate"></mat-progress-bar>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div style="display: flex; justify-content: center;" *ngIf="downloads && downloads.length > 0 && !autoplay">
|
@if (downloads && downloads.length > 0 && !autoplay) {
|
||||||
|
<div style="display: flex; justify-content: center;">
|
||||||
<app-downloads style="width: 80%; min-width: 350px; 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">
|
@if (cachedFileManagerEnabled || fileManagerEnabled) {
|
||||||
<app-recent-videos #recentVideos></app-recent-videos>
|
<app-recent-videos #recentVideos></app-recent-videos>
|
||||||
<br/>
|
<br/>
|
||||||
<h4 style="text-align: center">Custom playlists</h4>
|
<h4 style="text-align: center">Custom playlists</h4>
|
||||||
<app-custom-playlists></app-custom-playlists>
|
<app-custom-playlists></app-custom-playlists>
|
||||||
</ng-container>
|
}
|
||||||
|
|
||||||
@@ -169,6 +169,8 @@ export class MainComponent implements OnInit {
|
|||||||
argsChangedSubject: Subject<boolean> = new Subject<boolean>();
|
argsChangedSubject: Subject<boolean> = new Subject<boolean>();
|
||||||
simulatedOutput = '';
|
simulatedOutput = '';
|
||||||
|
|
||||||
|
interval_id = null;
|
||||||
|
|
||||||
constructor(public postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
|
constructor(public postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
|
||||||
private router: Router, public dialog: MatDialog, private platform: Platform, private route: ActivatedRoute) {
|
private router: Router, public dialog: MatDialog, private platform: Platform, private route: ActivatedRoute) {
|
||||||
this.audioOnly = false;
|
this.audioOnly = false;
|
||||||
@@ -232,11 +234,12 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get downloads routine
|
// get downloads routine
|
||||||
setInterval(() => {
|
if (this.interval_id) { clearInterval(this.interval_id) }
|
||||||
|
this.interval_id = setInterval(() => {
|
||||||
if (this.current_download) {
|
if (this.current_download) {
|
||||||
this.getCurrentDownload();
|
this.getCurrentDownload();
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 1000);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -294,6 +297,10 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.interval_id) { clearInterval(this.interval_id) }
|
||||||
|
}
|
||||||
|
|
||||||
// download helpers
|
// download helpers
|
||||||
downloadHelper(container: DatabaseFile | Playlist, type: string, is_playlist = false, force_view = false, navigate_mode = false): void {
|
downloadHelper(container: DatabaseFile | Playlist, type: string, is_playlist = false, force_view = false, navigate_mode = false): void {
|
||||||
this.downloadingfile = false;
|
this.downloadingfile = false;
|
||||||
@@ -541,7 +548,7 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
if (!(this.cachedAvailableFormats[url] && this.cachedAvailableFormats[url]['formats'])) {
|
if (!(this.cachedAvailableFormats[url] && this.cachedAvailableFormats[url]['formats'])) {
|
||||||
this.cachedAvailableFormats[url]['formats_loading'] = true;
|
this.cachedAvailableFormats[url]['formats_loading'] = true;
|
||||||
this.postsService.getFileFormats([url]).subscribe(res => {
|
this.postsService.getFileFormats(url).subscribe(res => {
|
||||||
this.cachedAvailableFormats[url]['formats_loading'] = false;
|
this.cachedAvailableFormats[url]['formats_loading'] = false;
|
||||||
const infos = res['result'];
|
const infos = res['result'];
|
||||||
if (!infos || !infos.formats) {
|
if (!infos || !infos.formats) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<div style="height: 100%" *ngIf="playlist.length > 0 && show_player">
|
@if (playlist.length > 0 && show_player) {
|
||||||
|
<div style="height: 100%">
|
||||||
<div style="height: 100%" [ngClass]="(currentItem.type === 'audio/mp3') ? null : 'container-video'">
|
<div style="height: 100%" [ngClass]="(currentItem.type === 'audio/mp3') ? null : 'container-video'">
|
||||||
<div style="max-width: 100%; margin-left: 0px; height: 100%">
|
<div style="max-width: 100%; margin-left: 0px; height: 100%">
|
||||||
<mat-drawer-container style="height: 100%" class="example-container" autosize>
|
<mat-drawer-container style="height: 100%" class="example-container" autosize>
|
||||||
@@ -6,59 +7,80 @@
|
|||||||
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(currentItem.type === 'audio/mp3') ? postsService.theme.drawer_color : 'black'">
|
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(currentItem.type === 'audio/mp3') ? postsService.theme.drawer_color : 'black'">
|
||||||
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="$any(media)" [src]="currentItem.src" id="singleVideo" preload="auto" controls playsinline>
|
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="$any(media)" [src]="currentItem.src" id="singleVideo" preload="auto" controls playsinline>
|
||||||
</video>
|
</video>
|
||||||
<app-skip-ad-button *ngIf="postsService['config']['API']['use_sponsorblock_API'] && api && playlist?.length > 0 && playlist[currentIndex]['type'] === 'video/mp4'" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [current_video]="playlist[currentIndex]" [playback_timestamp]="api.currentTime" class="skip-ad-button"></app-skip-ad-button>
|
@if (postsService['config']['API']['use_sponsorblock_API'] && api && playlist?.length > 0 && playlist[currentIndex]['type'] === 'video/mp4') {
|
||||||
|
<app-skip-ad-button (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [current_video]="playlist[currentIndex]" [playback_timestamp]="api.currentTime" class="skip-ad-button"></app-skip-ad-button>
|
||||||
|
}
|
||||||
</vg-player>
|
</vg-player>
|
||||||
</div>
|
</div>
|
||||||
<div style="height: fit-content; width: 100%; margin-top: 10px;">
|
<div style="height: fit-content; width: 100%; margin-top: 10px;">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-2 col-lg-1">
|
<div class="col-2 col-lg-1">
|
||||||
<ng-container *ngIf="db_file">{{db_file['local_view_count'] ? db_file['local_view_count']+1 : 1}} <ng-container i18n="View count label">views</ng-container></ng-container>
|
@if (db_file) {
|
||||||
|
{{db_file['local_view_count'] ? db_file['local_view_count']+1 : 1}} <ng-container i18n="View count label">views</ng-container>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div style="white-space: pre-line;" class="col-8 col-lg-9">
|
<div style="white-space: pre-line;" class="col-8 col-lg-9">
|
||||||
<ng-container *ngIf="db_file && db_file['description']">
|
@if (db_file && db_file['description']) {
|
||||||
<p>
|
<p>
|
||||||
<app-see-more [text]="db_file['description']"></app-see-more>
|
<app-see-more [text]="db_file['description']"></app-see-more>
|
||||||
</p>
|
</p>
|
||||||
</ng-container>
|
} @else {
|
||||||
<ng-container *ngIf="!db_file || !db_file['description']">
|
|
||||||
<p i18n="No description" style="text-align: center;">
|
<p i18n="No description" style="text-align: center;">
|
||||||
No description available.
|
No description available.
|
||||||
</p>
|
</p>
|
||||||
</ng-container>
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<span class="buttons" *ngIf="db_playlist">
|
@if (db_playlist) {
|
||||||
|
<span class="buttons">
|
||||||
<button (click)="downloadContent()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon></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>
|
@if (downloading) {
|
||||||
<button *ngIf="(!postsService.isLoggedIn || postsService.permissions.includes('sharing')) && !auto" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
<mat-spinner class="spinner" [diameter]="35"></mat-spinner>
|
||||||
|
}
|
||||||
|
@if ((!postsService.isLoggedIn || postsService.permissions.includes('sharing')) && !auto) {
|
||||||
|
<button (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
<span class="buttons" *ngIf="db_file">
|
}
|
||||||
|
@if (db_file) {
|
||||||
|
<span class="buttons">
|
||||||
<button (click)="downloadFile()" [disabled]="downloading" mat-icon-button><mat-icon>cloud_download</mat-icon></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>
|
@if (downloading) {
|
||||||
<button *ngIf="!postsService.isLoggedIn || postsService.permissions.includes('sharing')" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
<mat-spinner class="spinner" [diameter]="35"></mat-spinner>
|
||||||
|
}
|
||||||
|
@if (!postsService.isLoggedIn || postsService.permissions.includes('sharing')) {
|
||||||
|
<button (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
<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>
|
@if (db_file || db_playlist) {
|
||||||
<button *ngIf="db_file && db_file.url.includes('twitch.tv')" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
|
<button (click)="openFileInfoDialog()" mat-icon-button><mat-icon>info</mat-icon></button>
|
||||||
|
}
|
||||||
|
@if (db_file && db_file.url.includes('twitch.tv')) {
|
||||||
|
<button (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="height: fit-content; width: 100%; margin-top: 10px;">
|
<div style="height: fit-content; width: 100%; margin-top: 10px;">
|
||||||
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="true" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="true" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||||
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
|
@for (playlist_item of playlist; track playlist_item; let i = $index) {
|
||||||
|
<mat-button-toggle cdkDrag [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
|
||||||
|
}
|
||||||
</mat-button-toggle-group>
|
</mat-button-toggle-group>
|
||||||
</div>
|
</div>
|
||||||
|
@if (db_file && api && postsService.config) {
|
||||||
<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 (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']">
|
<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')">
|
@if (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>
|
}
|
||||||
</mat-drawer>
|
</mat-drawer>
|
||||||
</mat-drawer-container>
|
</mat-drawer-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-m
|
|||||||
import { DatabaseFile, FileType, Playlist } from '../../api-types';
|
import { DatabaseFile, FileType, Playlist } from '../../api-types';
|
||||||
import { TwitchChatComponent } from 'app/components/twitch-chat/twitch-chat.component';
|
import { TwitchChatComponent } from 'app/components/twitch-chat/twitch-chat.component';
|
||||||
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
export interface IMedia {
|
export interface IMedia {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'rxjs/add/operator/map';
|
|||||||
import 'rxjs/add/operator/catch';
|
import 'rxjs/add/operator/catch';
|
||||||
import 'rxjs/add/observable/throw';
|
import 'rxjs/add/observable/throw';
|
||||||
import { THEMES_CONFIG } from '../themes';
|
import { THEMES_CONFIG } from '../themes';
|
||||||
import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router';
|
import { Router, 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';
|
||||||
@@ -113,14 +113,16 @@ import {
|
|||||||
Archive,
|
Archive,
|
||||||
Subscription,
|
Subscription,
|
||||||
RestartDownloadResponse,
|
RestartDownloadResponse,
|
||||||
TaskType
|
TaskType,
|
||||||
|
CheckSubscriptionRequest
|
||||||
} from '../api-types';
|
} from '../api-types';
|
||||||
import { isoLangs } from './dialogs/user-profile-dialog/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';
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostsService implements CanActivate {
|
export class PostsService {
|
||||||
path = '';
|
path = '';
|
||||||
|
|
||||||
// local settings
|
// local settings
|
||||||
@@ -175,7 +177,7 @@ export class PostsService implements CanActivate {
|
|||||||
|
|
||||||
if (isDevMode()) {
|
if (isDevMode()) {
|
||||||
this.debugMode = true;
|
this.debugMode = true;
|
||||||
this.path = 'http://localhost:17442/api/';
|
this.path = !environment.codespaces ? 'http://localhost:17442/api/' : `${window.location.origin.replace('4200', '17442')}/api/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.http_params = `apiKey=${this.auth_token}`
|
this.http_params = `apiKey=${this.auth_token}`
|
||||||
@@ -458,7 +460,7 @@ export class PostsService implements CanActivate {
|
|||||||
return this.http.post<SuccessObject>(this.path + 'deleteArchiveItems', body, this.httpOptions);
|
return this.http.post<SuccessObject>(this.path + 'deleteArchiveItems', body, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileFormats(url) {
|
getFileFormats(url: string) {
|
||||||
const body: GetFileFormatsRequest = {url: url};
|
const body: GetFileFormatsRequest = {url: url};
|
||||||
return this.http.post<GetFileFormatsResponse>(this.path + 'getFileFormats', body, this.httpOptions);
|
return this.http.post<GetFileFormatsResponse>(this.path + 'getFileFormats', body, this.httpOptions);
|
||||||
}
|
}
|
||||||
@@ -566,8 +568,18 @@ export class PostsService implements CanActivate {
|
|||||||
return this.http.post<SuccessObject>(this.path + 'updateSubscription', {subscription: subscription}, this.httpOptions);
|
return this.http.post<SuccessObject>(this.path + 'updateSubscription', {subscription: subscription}, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe(sub: SubscriptionRequestData, deleteMode = false) {
|
checkSubscription(sub_id: string) {
|
||||||
const body: UnsubscribeRequest = {sub: sub, deleteMode: deleteMode};
|
const body: CheckSubscriptionRequest = {sub_id: sub_id};
|
||||||
|
return this.http.post<SuccessObject>(this.path + 'checkSubscription', body, this.httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCheckSubscription(sub_id: string) {
|
||||||
|
const body: CheckSubscriptionRequest = {sub_id: sub_id};
|
||||||
|
return this.http.post<SuccessObject>(this.path + 'cancelCheckSubscription', body, this.httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(sub_id: string, deleteMode = false) {
|
||||||
|
const body: UnsubscribeRequest = {sub_id: sub_id, deleteMode: deleteMode};
|
||||||
return this.http.post<UnsubscribeResponse>(this.path + 'unsubscribe', body, this.httpOptions)
|
return this.http.post<UnsubscribeResponse>(this.path + 'unsubscribe', body, this.httpOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
<!-- Server -->
|
<!-- Server -->
|
||||||
<mat-tab label="Main" i18n-label="Main settings label">
|
<mat-tab label="Main" i18n-label="Main settings label">
|
||||||
<ng-template matTabContent style="padding: 15px;">
|
<ng-template matTabContent style="padding: 15px;">
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
@if (new_config) {
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-form-field class="text-field" color="accent">
|
<mat-form-field class="text-field" color="accent">
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['multi_user_mode']"><ng-container i18n="Multi user mode setting">Multi-user mode</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['multi_user_mode']"><ng-container i18n="Multi user mode setting">Multi-user mode</ng-container></mat-checkbox>
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Subscriptions']['allow_subscriptions']"><ng-container i18n="Allow subscriptions setting">Allow subscriptions</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['Subscriptions']['allow_subscriptions']"><ng-container i18n="Allow subscriptions setting">Allow subscriptions</ng-container></mat-checkbox>
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
@@ -78,13 +79,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<!-- Downloader -->
|
<!-- Downloader -->
|
||||||
<mat-tab label="Downloader" i18n-label="Downloader settings label">
|
<mat-tab label="Downloader" i18n-label="Downloader settings label">
|
||||||
<ng-template matTabContent>
|
<ng-template matTabContent>
|
||||||
<!-- Downloader -->
|
<!-- Downloader -->
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
@if (new_config) {
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-form-field class="text-field" color="accent">
|
<mat-form-field class="text-field" color="accent">
|
||||||
@@ -93,7 +96,6 @@
|
|||||||
<mat-hint><ng-container i18n="Aduio path setting input hint">Path for audio only downloads. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
|
<mat-hint><ng-container i18n="Aduio path setting input hint">Path for audio only downloads. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-form-field class="text-field" color="accent">
|
<mat-form-field class="text-field" color="accent">
|
||||||
<mat-label i18n="Video folder path">Video folder path</mat-label>
|
<mat-label i18n="Video folder path">Video folder path</mat-label>
|
||||||
@@ -101,7 +103,6 @@
|
|||||||
<mat-hint><ng-container i18n="Video path setting input hint">Path for video downloads. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
|
<mat-hint><ng-container i18n="Video path setting input hint">Path for video downloads. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mt-3 mb-1">
|
<div class="col-12 mt-3 mb-1">
|
||||||
<mat-form-field class="text-field" color="accent">
|
<mat-form-field class="text-field" color="accent">
|
||||||
<mat-label i18n="Default file output">Default file output</mat-label>
|
<mat-label i18n="Default file output">Default file output</mat-label>
|
||||||
@@ -112,7 +113,6 @@
|
|||||||
</mat-hint>
|
</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mt-4 mb-5">
|
<div class="col-12 mt-4 mb-5">
|
||||||
<mat-form-field class="text-field" style="margin-right: 12px;" color="accent">
|
<mat-form-field class="text-field" style="margin-right: 12px;" color="accent">
|
||||||
<mat-label i18n="Global custom args">Global custom args</mat-label>
|
<mat-label i18n="Global custom args">Global custom args</mat-label>
|
||||||
@@ -124,12 +124,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<h6 i18n="Categories">Categories</h6>
|
<h6 i18n="Categories">Categories</h6>
|
||||||
<div *ngIf="postsService.categories && postsService.categories.length > 0" cdkDropList class="category-list" (cdkDropListDropped)="dropCategory($event)">
|
@if (postsService.categories && postsService.categories.length > 0) {
|
||||||
<div class="category-box" *ngFor="let category of postsService.categories" cdkDrag>
|
<div cdkDropList class="category-list" (cdkDropListDropped)="dropCategory($event)">
|
||||||
|
@for (category of postsService.categories; track category) {
|
||||||
|
<div class="category-box" cdkDrag>
|
||||||
<div class="category-custom-placeholder" *cdkDragPlaceholder></div>
|
<div class="category-custom-placeholder" *cdkDragPlaceholder></div>
|
||||||
{{category['name']}}
|
{{category['name']}}
|
||||||
<span style="float: right">
|
<span style="float: right">
|
||||||
@@ -137,7 +139,9 @@
|
|||||||
<button mat-icon-button (click)="deleteCategory(category)"><mat-icon>cancel</mat-icon></button>
|
<button mat-icon-button (click)="deleteCategory(category)"><mat-icon>cancel</mat-icon></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<button style="margin-top: 10px;" mat-mini-fab (click)="openAddCategoryDialog()"><mat-icon>add</mat-icon></button>
|
<button style="margin-top: 10px;" mat-mini-fab (click)="openAddCategoryDialog()"><mat-icon>add</mat-icon></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-2 mb-2">
|
<div class="col-12 mt-2 mb-2">
|
||||||
@@ -146,23 +150,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mt-2">
|
<div class="col-12 mt-2">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_thumbnail']"><ng-container i18n="Include thumbnail setting">Include thumbnail</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_thumbnail']"><ng-container i18n="Include thumbnail setting">Include thumbnail</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mt-2 mb-2">
|
<div class="col-12 mt-2 mb-2">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_metadata']"><ng-container i18n="Include metadata setting">Include metadata</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_metadata']"><ng-container i18n="Include metadata setting">Include metadata</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3 mb-4">
|
<div class="col-12 mt-3 mb-4">
|
||||||
<mat-form-field class="text-field" color="accent">
|
<mat-form-field class="text-field" color="accent">
|
||||||
@@ -181,19 +183,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<button (click)="killAllDownloads()" mat-stroked-button color="warn"><ng-container i18n="Kill all downloads button">Kill all downloads</ng-container></button>
|
<button (click)="killAllDownloads()" mat-stroked-button color="warn"><ng-container i18n="Kill all downloads button">Kill all downloads</ng-container></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<!-- Extra -->
|
<!-- Extra -->
|
||||||
<mat-tab label="Extra" i18n-label="Extra settings label">
|
<mat-tab label="Extra" i18n-label="Extra settings label">
|
||||||
<ng-template matTabContent>
|
<ng-template matTabContent>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
@if (new_config) {
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-form-field class="text-field" color="accent">
|
<mat-form-field class="text-field" color="accent">
|
||||||
@@ -220,7 +224,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_API_key']"><ng-container i18n="Enable Public API key setting">Enable Public API</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_API_key']"><ng-container i18n="Enable Public API key setting">Enable Public API</ng-container></mat-checkbox>
|
||||||
@@ -240,7 +244,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_youtube_API']"><ng-container i18n="Use YouTube API setting">Use YouTube API</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_youtube_API']"><ng-container i18n="Use YouTube API setting">Use YouTube API</ng-container></mat-checkbox>
|
||||||
@@ -264,7 +268,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<h6>RSS Feed</h6>
|
<h6>RSS Feed</h6>
|
||||||
@@ -276,7 +280,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<h6>Chrome</h6>
|
<h6>Chrome</h6>
|
||||||
@@ -299,50 +303,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<!-- Database -->
|
<!-- Database -->
|
||||||
<mat-tab label="Database" i18n-label="Database settings label">
|
<mat-tab label="Database" i18n-label="Database settings label">
|
||||||
<ng-template matTabContent>
|
<ng-template matTabContent>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
@if (new_config) {
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<div *ngIf="db_info">
|
@if (db_info) {
|
||||||
|
<div>
|
||||||
<p><ng-container i18n="Database location label">Database location:</ng-container> <strong>{{db_info['using_local_db'] ? 'Local' : 'MongoDB'}}</strong></p>
|
<p><ng-container i18n="Database location label">Database location:</ng-container> <strong>{{db_info['using_local_db'] ? 'Local' : 'MongoDB'}}</strong></p>
|
||||||
<h6 i18n="Records per table label">Records per table</h6>
|
<h6 i18n="Records per table label">Records per table</h6>
|
||||||
<mat-list style="padding-top: 0px">
|
<mat-list style="padding-top: 0px">
|
||||||
<mat-list-item style="height: 28px" *ngFor="let table_stats of db_info['stats_by_table'] | keyvalue">
|
@for (table_stats of db_info['stats_by_table'] | keyvalue; track table_stats) {
|
||||||
|
<mat-list-item style="height: 28px">
|
||||||
{{table_stats.key}}: {{table_stats.value.records_count}}
|
{{table_stats.key}}: {{table_stats.value.records_count}}
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
|
}
|
||||||
</mat-list>
|
</mat-list>
|
||||||
|
|
||||||
<mat-form-field style="width: 100%; margin-top: 15px; margin-bottom: 10px" color="accent">
|
<mat-form-field style="width: 100%; margin-top: 15px; margin-bottom: 10px" color="accent">
|
||||||
<mat-label i18n="MongoDB Connection String">MongoDB Connection String</mat-label>
|
<mat-label i18n="MongoDB Connection String">MongoDB Connection String</mat-label>
|
||||||
<input [(ngModel)]="new_config['Database']['mongodb_connection_string']" matInput required>
|
<input [(ngModel)]="new_config['Database']['mongodb_connection_string']" matInput required>
|
||||||
<mat-hint><ng-container i18n="MongoDB Connection String setting hint AKA preamble">Example:</ng-container> mongodb://127.0.0.1:27017/?compressors=zlib<br>Docker: mongodb://<container name>:27017/?compressors=zlib</mat-hint>
|
<mat-hint><ng-container i18n="MongoDB Connection String setting hint AKA preamble">Example:</ng-container> mongodb://127.0.0.1:27017/?compressors=zlib<br>Docker: mongodb://<container name>:27017/?compressors=zlib</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<div class="test-connection-div">
|
<div class="test-connection-div">
|
||||||
<button (click)="testConnectionString(new_config['Database']['mongodb_connection_string'])" [disabled]="testing_connection_string" mat-flat-button color="accent"><ng-container i18n="Test connection string button">Test connection string</ng-container></button>
|
<button (click)="testConnectionString(new_config['Database']['mongodb_connection_string'])" [disabled]="testing_connection_string" mat-flat-button color="accent"><ng-container i18n="Test connection string button">Test connection string</ng-container></button>
|
||||||
<mat-spinner class="test-connection-spinner" style="margin-left: 10px" *ngIf="testing_connection_string" [diameter]="25"></mat-spinner>
|
@if (testing_connection_string) {
|
||||||
|
<mat-spinner class="test-connection-spinner" style="margin-left: 10px" [diameter]="25"></mat-spinner>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="transfer-db-div">
|
<div class="transfer-db-div">
|
||||||
<button [disabled]="db_transferring" color="accent" (click)="transferDB()" mat-raised-button><ng-container i18n="Transfer DB button">Transfer DB to </ng-container>{{db_info['using_local_db'] ? 'MongoDB' : 'Local'}}</button>
|
<button [disabled]="db_transferring" color="accent" (click)="transferDB()" mat-raised-button><ng-container i18n="Transfer DB button">Transfer DB to </ng-container>{{db_info['using_local_db'] ? 'MongoDB' : 'Local'}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!db_info">
|
} @else {
|
||||||
|
<div>
|
||||||
<ng-container i18n="Database info not retrieved error message">Database information could not be retrieved. Check the server logs for more information.</ng-container>
|
<ng-container i18n="Database info not retrieved error message">Database information could not be retrieved. Check the server logs for more information.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
<mat-tab label="Notifications" i18n-label="Notifications settings label">
|
<mat-tab label="Notifications" i18n-label="Notifications settings label">
|
||||||
<ng-template matTabContent>
|
<ng-template matTabContent>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
@if (new_config) {
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<div><a target="_blank" href="https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Notifications"><ng-container i18n="Documentation">Documentation</ng-container></a></div>
|
<div><a target="_blank" href="https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Notifications"><ng-container i18n="Documentation">Documentation</ng-container></a></div>
|
||||||
@@ -426,14 +438,23 @@
|
|||||||
<mat-hint><a target="_blank" href="https://stackoverflow.com/a/37396871/8088021"><ng-container i18n="Telegram chat ID help">How do I get the chat ID?</ng-container></a></mat-hint>
|
<mat-hint><a target="_blank" href="https://stackoverflow.com/a/37396871/8088021"><ng-container i18n="Telegram chat ID help">How do I get the chat ID?</ng-container></a></mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 mb-2">
|
||||||
|
<mat-form-field class="text-field" color="accent">
|
||||||
|
<mat-label i18n="Telegram webhook proxy">Telegram webhook proxy</mat-label>
|
||||||
|
<input placeholder="https://smee.io/XXXXX" [disabled]="!new_config['Extra']['enable_notifications'] || !new_config['API']['use_telegram_API']" [(ngModel)]="new_config['API']['telegram_webhook_proxy']" matInput>
|
||||||
|
<mat-hint><a target="_blank" href="https://smee.io/"><ng-container i18n="Telegram webhook proxy help">Example service</ng-container></a></mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<!-- Advanced -->
|
<!-- Advanced -->
|
||||||
<mat-tab label="Advanced" i18n-label="Host settings label">
|
<mat-tab label="Advanced" i18n-label="Host settings label">
|
||||||
<ng-template matTabContent>
|
<ng-template matTabContent>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
@if (new_config) {
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
@@ -493,7 +514,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-2 mb-2">
|
<div class="col-12 mt-2 mb-2">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['use_cookies']"><ng-container i18n="Use cookies setting">Use Cookies</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['use_cookies']"><ng-container i18n="Use cookies setting">Use Cookies</ng-container></mat-checkbox>
|
||||||
@@ -502,17 +523,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid mt-3">
|
<div class="container-fluid mt-3">
|
||||||
<app-updater></app-updater>
|
<app-updater></app-updater>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-4">
|
<div class="col-12 mt-4">
|
||||||
<button (click)="restartServer()" mat-stroked-button color="warn"><ng-container i18n="Restart server button">Restart server</ng-container></button>
|
<button (click)="restartServer()" mat-stroked-button color="warn"><ng-container i18n="Restart server button">Restart server</ng-container></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<mat-tab [disabled]="!postsService.config?.Advanced.multi_user_mode">
|
<mat-tab [disabled]="!postsService.config?.Advanced.multi_user_mode">
|
||||||
@@ -521,8 +543,9 @@
|
|||||||
<ng-container i18n="Users settings label">Users</ng-container>
|
<ng-container i18n="Users settings label">Users</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-container *ngIf="postsService.config?.Advanced.multi_user_mode">
|
@if (postsService.config?.Advanced.multi_user_mode) {
|
||||||
<div *ngIf="new_config" style="margin-top: 24px; margin-bottom: -25px;">
|
@if (new_config) {
|
||||||
|
<div style="margin-top: 24px; margin-bottom: -25px;">
|
||||||
<div>
|
<div>
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -538,7 +561,8 @@
|
|||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div *ngIf="new_config['Users']['auth_method'] === 'ldap'">
|
@if (new_config['Users']['auth_method'] === 'ldap') {
|
||||||
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label i18n="LDAP URL">LDAP URL</mat-label>
|
<mat-label i18n="LDAP URL">LDAP URL</mat-label>
|
||||||
@@ -570,20 +594,23 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
</div>
|
</div>
|
||||||
<app-modify-users *ngIf="new_config"></app-modify-users>
|
<app-modify-users></app-modify-users>
|
||||||
</ng-container>
|
}
|
||||||
|
}
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<mat-tab *ngIf="postsService.config" label="Logs" i18n-label="Logs settings label">
|
@if (postsService.config) {
|
||||||
|
<mat-tab label="Logs" i18n-label="Logs settings label">
|
||||||
<ng-template matTabContent>
|
<ng-template matTabContent>
|
||||||
<div style="margin-top: 15px; height: 84%;">
|
<div style="margin-top: 15px; height: 84%;">
|
||||||
<app-logs-viewer></app-logs-viewer>
|
<app-logs-viewer></app-logs-viewer>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
}
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button style="margin-left: 10px; height: 37.3px" color="accent" (click)="saveSettings()" [disabled]="settingsSame()" mat-raised-button><mat-icon>done</mat-icon>
|
<button style="margin-left: 10px; height: 37.3px" color="accent" (click)="saveSettings()" [disabled]="settingsSame()" mat-raised-button><mat-icon>done</mat-icon>
|
||||||
<ng-container i18n="Settings save button">Save</ng-container>
|
<ng-container i18n="Settings save button">Save</ng-container>
|
||||||
@@ -592,4 +619,3 @@
|
|||||||
<span i18n="Settings cancel button">Cancel</span>
|
<span i18n="Settings cancel button">Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,38 @@
|
|||||||
<div style="margin-top: 14px;">
|
<div style="margin-top: 14px;">
|
||||||
<button class="back-button" (click)="goBack()" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
<button class="back-button" (click)="goBack()" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||||
<div style="margin-bottom: 15px;">
|
<div style="margin-bottom: 15px;">
|
||||||
<h2 style="text-align: center;" *ngIf="subscription">
|
@if (subscription) {
|
||||||
{{subscription.name}} <ng-container *ngIf="subscription.paused" i18n="Paused suffix">(Paused)</ng-container>
|
<h2 style="text-align: center;">
|
||||||
|
{{subscription.name}}
|
||||||
|
@if (subscription.paused) {
|
||||||
|
<ng-container i18n="Paused suffix">(Paused)</ng-container>
|
||||||
|
}
|
||||||
|
<button class="edit-button" (click)="editSubscription()" [disabled]="downloading" matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button><mat-icon class="save-icon">edit</mat-icon></button>
|
||||||
</h2>
|
</h2>
|
||||||
<mat-progress-bar style="width: 80%; margin: 0 auto; margin-top: 15px;" *ngIf="subscription && subscription.downloading" mode="indeterminate"></mat-progress-bar>
|
}
|
||||||
|
@if (subscription && subscription.downloading) {
|
||||||
|
<mat-progress-bar style="width: 80%; margin: 0 auto; margin-top: 15px;" mode="indeterminate"></mat-progress-bar>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
|
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<!-- Extra margin added for floating buttons to have room -->
|
<!-- Extra margin added for floating buttons to have room -->
|
||||||
<div style="margin-bottom: 100px;" *ngIf="subscription">
|
@if (subscription) {
|
||||||
|
<div style="margin-bottom: 100px;">
|
||||||
<app-recent-videos #recentVideos [sub_id]="subscription.id"></app-recent-videos>
|
<app-recent-videos #recentVideos [sub_id]="subscription.id"></app-recent-videos>
|
||||||
</div>
|
</div>
|
||||||
<button class="edit-button" color="primary" (click)="editSubscription()" [disabled]="downloading" matTooltip="Edit" i18n-matTooltip="Edit" mat-fab><mat-icon class="save-icon">edit</mat-icon></button>
|
}
|
||||||
<button class="watch-button" color="primary" (click)="watchSubscription()" matTooltip="Play all" i18n-matTooltip="Play all" mat-fab><mat-icon class="save-icon">video_library</mat-icon></button>
|
<div class="check-button">
|
||||||
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" matTooltip="Download zip" i18n-matTooltip="Download zip" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
@if (subscription.downloading) {
|
||||||
|
<button color="primary" (click)="cancelCheckSubscription()" [disabled]="cancel_clicked" matTooltip="Cancel subscription check" i18n-matTooltip="Cancel subscription check" mat-fab><mat-icon class="save-icon">cancel</mat-icon></button>
|
||||||
|
} @else {
|
||||||
|
<button color="primary" (click)="checkSubscription()" [disabled]="check_clicked" matTooltip="Check subscription" i18n-matTooltip="Check subscription" mat-fab><mat-icon class="save-icon">youtube_searched_for</mat-icon></button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<button class="watch-button" color="primary" (click)="watchSubscription()" matTooltip="Play all" i18n-matTooltip="Play all" mat-fab><mat-icon class="save-icon">video_library</mat-icon></button>
|
||||||
|
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" matTooltip="Download zip" i18n-matTooltip="Download zip" mat-fab><mat-icon class="save-icon">save</mat-icon>
|
||||||
|
@if (downloading) {
|
||||||
|
<mat-spinner class="spinner" [diameter]="50"></mat-spinner>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,13 +58,19 @@
|
|||||||
bottom: 25px;
|
bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-button {
|
.check-button {
|
||||||
left: 25px;
|
left: 25px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 25px;
|
bottom: 25px;
|
||||||
z-index: 99999;
|
z-index: 99999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-button {
|
||||||
|
right: 35px;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
.save-icon {
|
.save-icon {
|
||||||
bottom: 1px;
|
bottom: 1px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { PostsService } from 'app/posts.services';
|
|||||||
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
|
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
|
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
|
||||||
|
import { Subscription } from 'api-types';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-subscription',
|
selector: 'app-subscription',
|
||||||
@@ -12,11 +14,13 @@ import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-d
|
|||||||
export class SubscriptionComponent implements OnInit, OnDestroy {
|
export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
id = null;
|
id = null;
|
||||||
subscription = null;
|
subscription: Subscription = null;
|
||||||
use_youtubedl_archive = false;
|
use_youtubedl_archive = false;
|
||||||
descendingMode = true;
|
descendingMode = true;
|
||||||
downloading = false;
|
downloading = false;
|
||||||
sub_interval = null;
|
sub_interval = null;
|
||||||
|
check_clicked = false;
|
||||||
|
cancel_clicked = false;
|
||||||
|
|
||||||
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { }
|
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { }
|
||||||
|
|
||||||
@@ -90,4 +94,34 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate(['/player', {sub_id: this.subscription.id}])
|
this.router.navigate(['/player', {sub_id: this.subscription.id}])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkSubscription(): void {
|
||||||
|
this.check_clicked = true;
|
||||||
|
this.postsService.checkSubscription(this.subscription.id).subscribe(res => {
|
||||||
|
this.check_clicked = false;
|
||||||
|
if (!res['success']) {
|
||||||
|
this.postsService.openSnackBar('Failed to check subscription!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
console.error(err);
|
||||||
|
this.check_clicked = false;
|
||||||
|
this.postsService.openSnackBar('Failed to check subscription!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCheckSubscription(): void {
|
||||||
|
this.cancel_clicked = true;
|
||||||
|
this.postsService.cancelCheckSubscription(this.subscription.id).subscribe(res => {
|
||||||
|
this.cancel_clicked = false;
|
||||||
|
if (!res['success']) {
|
||||||
|
this.postsService.openSnackBar('Failed to cancel check subscription!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
console.error(err);
|
||||||
|
this.cancel_clicked = false;
|
||||||
|
this.postsService.openSnackBar('Failed to cancel check subscription!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<h2 i18n="Subscriptions title" style="text-align: center; margin-bottom: 15px;">Your subscriptions</h2>
|
<h2 i18n="Subscriptions title" style="text-align: center; margin-bottom: 15px;">Your subscriptions</h2>
|
||||||
|
|
||||||
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
|
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<h4 i18n="Subscriptions channels title" style="text-align: center;">Channels</h4>
|
<h4 i18n="Subscriptions channels title" style="text-align: center;">Channels</h4>
|
||||||
<mat-nav-list class="sub-nav-list">
|
<mat-nav-list class="sub-nav-list">
|
||||||
<mat-list-item *ngFor="let sub of channel_subscriptions" style="pointer-events: none">
|
@for (sub of channel_subscriptions; track sub) {
|
||||||
|
<mat-list-item style="pointer-events: none">
|
||||||
<a style="pointer-events: auto;" class="a-list-item" matListItemTitle (click)="goToSubscription(sub)">
|
<a style="pointer-events: auto;" class="a-list-item" matListItemTitle (click)="goToSubscription(sub)">
|
||||||
<strong *ngIf="sub.name">{{ sub.name }} <ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></strong>
|
@if (sub.name) {
|
||||||
<div *ngIf="!sub.name">
|
<strong>{{ sub.name }}
|
||||||
|
@if (sub.paused) {
|
||||||
|
<ng-container i18n="Paused suffix">(Paused)</ng-container>
|
||||||
|
}
|
||||||
|
</strong>
|
||||||
|
} @else {
|
||||||
|
<div>
|
||||||
<ng-container i18n="Subscription playlist not available text">Name not available. Channel retrieval in progress.</ng-container>
|
<ng-container i18n="Subscription playlist not available text">Name not available. Channel retrieval in progress.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</a>
|
</a>
|
||||||
<div style="pointer-events: auto; color: unset" matListItemMeta>
|
<div style="pointer-events: auto; color: unset" matListItemMeta>
|
||||||
<button matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button (click)="editSubscription(sub)">
|
<button matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button (click)="editSubscription(sub)">
|
||||||
@@ -23,20 +28,29 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
|
}
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
|
@if (channel_subscriptions.length === 0 && subscriptions) {
|
||||||
<div style="width: 80%; margin: 0 auto; padding-left: 15px;" *ngIf="channel_subscriptions.length === 0 && subscriptions">
|
<div style="width: 80%; margin: 0 auto; padding-left: 15px;">
|
||||||
<p i18n="No channel subscriptions text">You have no channel subscriptions.</p>
|
<p i18n="No channel subscriptions text">You have no channel subscriptions.</p>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<h4 i18n="Subscriptions playlists title" style="text-align: center; margin-top: 10px;">Playlists</h4>
|
<h4 i18n="Subscriptions playlists title" style="text-align: center; margin-top: 10px;">Playlists</h4>
|
||||||
<mat-nav-list class="sub-nav-list">
|
<mat-nav-list class="sub-nav-list">
|
||||||
<mat-list-item *ngFor="let sub of playlist_subscriptions" style="pointer-events: none">
|
@for (sub of playlist_subscriptions; track sub) {
|
||||||
|
<mat-list-item style="pointer-events: none">
|
||||||
<a style="pointer-events: auto;" class="a-list-item" matListItemTitle (click)="goToSubscription(sub)">
|
<a style="pointer-events: auto;" class="a-list-item" matListItemTitle (click)="goToSubscription(sub)">
|
||||||
<strong *ngIf="sub.name">{{ sub.name }} <ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></strong>
|
@if (sub.name) {
|
||||||
<div *ngIf="!sub.name">
|
<strong>{{ sub.name }}
|
||||||
|
@if (sub.paused) {
|
||||||
|
<ng-container i18n="Paused suffix">(Paused)</ng-container>
|
||||||
|
}
|
||||||
|
</strong>
|
||||||
|
} @else {
|
||||||
|
<div>
|
||||||
<ng-container i18n="Subscription playlist not available text">Name not available. Channel retrieval in progress.</ng-container>
|
<ng-container i18n="Subscription playlist not available text">Name not available. Channel retrieval in progress.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</a>
|
</a>
|
||||||
<div style="pointer-events: auto; color: unset" matListItemMeta>
|
<div style="pointer-events: auto; color: unset" matListItemMeta>
|
||||||
<button matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button (click)="editSubscription(sub)">
|
<button matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button (click)="editSubscription(sub)">
|
||||||
@@ -47,14 +61,16 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
|
}
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
|
@if (playlist_subscriptions.length === 0 && subscriptions) {
|
||||||
<div style="width: 80%; margin: 0 auto; padding-left: 15px;" *ngIf="playlist_subscriptions.length === 0 && subscriptions">
|
<div style="width: 80%; margin: 0 auto; padding-left: 15px;">
|
||||||
<p i18n="No playlist subscriptions text">You have no playlist subscriptions.</p>
|
<p i18n="No playlist subscriptions text">You have no playlist subscriptions.</p>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div style="margin: 0 auto; width: 80%" *ngIf="subscriptions_loading">
|
@if (subscriptions_loading) {
|
||||||
|
<div style="margin: 0 auto; width: 80%">
|
||||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<button class="add-subscription-button" (click)="openSubscribeDialog()" matTooltip="Add subscription" i18n-matTooltip="Add subscription" mat-fab><mat-icon>add</mat-icon></button>
|
<button class="add-subscription-button" (click)="openSubscribeDialog()" matTooltip="Add subscription" i18n-matTooltip="Add subscription" mat-fab><mat-icon>add</mat-icon></button>
|
||||||
@@ -2,17 +2,27 @@
|
|||||||
<div style="display: inline-block">
|
<div style="display: inline-block">
|
||||||
<ng-container i18n="Select a version">Select a version:</ng-container>
|
<ng-container i18n="Select a version">Select a version:</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="availableVersions" style="display: inline-block; margin-left: 15px;">
|
@if (availableVersions) {
|
||||||
|
<div style="display: inline-block; margin-left: 15px;">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-select [(ngModel)]="selectedVersion">
|
<mat-select [(ngModel)]="selectedVersion">
|
||||||
<mat-option *ngFor="let version of availableVersionsFiltered" [value]="version['tag_name']">
|
@for (version of availableVersionsFiltered; track version) {
|
||||||
|
<mat-option [value]="version['tag_name']">
|
||||||
{{version['tag_name'] + (version === latestStableRelease ? ' - Latest Stable' : '') + (version['tag_name'] === CURRENT_VERSION ? ' - Current Version' : '')}}
|
{{version['tag_name'] + (version === latestStableRelease ? ' - Latest Stable' : '') + (version['tag_name'] === CURRENT_VERSION ? ' - Current Version' : '')}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="selectedVersion && selectedVersion !== CURRENT_VERSION" style="display: inline-block; margin-left: 15px;">
|
}
|
||||||
|
@if (selectedVersion && selectedVersion !== CURRENT_VERSION) {
|
||||||
|
<div style="display: inline-block; margin-left: 15px;">
|
||||||
<button (click)="updateServer()" color="accent" mat-raised-button><mat-icon>update</mat-icon>
|
<button (click)="updateServer()" color="accent" mat-raised-button><mat-icon>update</mat-icon>
|
||||||
<ng-container *ngIf="selectedVersion > CURRENT_VERSION">Upgrade to</ng-container><ng-container *ngIf="selectedVersion < CURRENT_VERSION">Downgrade to</ng-container> {{selectedVersion}}</button>
|
@if (selectedVersion > CURRENT_VERSION) {
|
||||||
|
<ng-container i18n="Upgrade to">Upgrade to</ng-container>
|
||||||
|
} @else {
|
||||||
|
<ng-container i18n="Downgrade to">Downgrade to</ng-container>
|
||||||
|
} {{selectedVersion}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -4142,6 +4142,163 @@
|
|||||||
<context context-type="linenumber">363</context>
|
<context context-type="linenumber">363</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="6549265851868599441" datatype="html">
|
||||||
|
<source>Video</source>
|
||||||
|
<target state="translated">Vídeo</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="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="c150a30bbbdb175b4d08820196a9acb66b167653" datatype="html">
|
||||||
|
<source>Archives empty</source>
|
||||||
|
<target state="translated">Arxius buits</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">Elimina seleccionat</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="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
|
||||||
|
<source>None</source>
|
||||||
|
<target state="translated">Cap</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="c41475a25c9f9d9639db9efa56637603a77528b4" datatype="html">
|
||||||
|
<source>Download archive</source>
|
||||||
|
<target state="translated">Descarregar arxiu</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="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">T'agradaria eliminar <x id="selected archives amount" equiv-text="this.selection.selected.length"/> arxiu(s)?</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">Elements de l'arxiu eliminats amb èxit!</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="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html">
|
||||||
|
<source>Archives</source>
|
||||||
|
<target state="translated">Arxius</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="4b3972c3e9485218508a95f7a4ce7758e3f09ced" datatype="html">
|
||||||
|
<source>Upload</source>
|
||||||
|
<target state="translated">Pujar</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="347407180135731058" datatype="html">
|
||||||
|
<source>Audio</source>
|
||||||
|
<target state="translated">Àudio</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">Suprimir</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">160</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8953483585652369683" datatype="html">
|
||||||
|
<source>Archive successfully imported!</source>
|
||||||
|
<target state="translated">Arxiu importat amb èxit!</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="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="3159807825117518005" datatype="html">
|
||||||
|
<source>Delete archives</source>
|
||||||
|
<target state="translated">Elimina arxius</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="5ca707824ab93066c7d9b44e1b8bf216725c2c22" datatype="html">
|
||||||
|
<source>Filter</source>
|
||||||
|
<target state="translated">Filtres</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>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -1107,7 +1107,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8" datatype="html">
|
<trans-unit id="dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8" datatype="html">
|
||||||
<source>Allow advanced download</source>
|
<source>Allow advanced download</source>
|
||||||
<target xml:lang="de-DE">Erweiterte Download-Optionen aktivieren</target>
|
<target xml:lang="de-DE" state="translated">Erweiterte Downloads erlauben</target>
|
||||||
<note from="description" priority="1">Allow advanced downloading setting</note>
|
<note from="description" priority="1">Allow advanced downloading setting</note>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">app/settings/settings.component.html</context>
|
<context context-type="sourcefile">app/settings/settings.component.html</context>
|
||||||
@@ -1525,7 +1525,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ea30873bd3f0d5e4fb2378eec3f0a1db77634a28" datatype="html">
|
<trans-unit id="ea30873bd3f0d5e4fb2378eec3f0a1db77634a28" datatype="html">
|
||||||
<source>Download all uploads</source>
|
<source>Download all uploads</source>
|
||||||
<target xml:lang="de-DE">Alle hochgeladene Videos herunterladen</target>
|
<target xml:lang="de-DE" state="translated">Alle hochgeladenen Videos herunterladen</target>
|
||||||
<note from="description" priority="1">Download all uploads subscription setting</note>
|
<note from="description" priority="1">Download all uploads subscription setting</note>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
|
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
|
||||||
@@ -1642,7 +1642,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="587b57ced54965d8874c3fd0e9dfedb987e5df04" datatype="html">
|
<trans-unit id="587b57ced54965d8874c3fd0e9dfedb987e5df04" datatype="html">
|
||||||
<source>You have no playlist subscriptions.</source>
|
<source>You have no playlist subscriptions.</source>
|
||||||
<target xml:lang="de-DE">Sie haben keine Wiedergabeliste abonniert.</target>
|
<target xml:lang="de-DE" state="translated">Du hast keine Wiedergabeliste abonniert.</target>
|
||||||
<note from="description" priority="1">No playlist subscriptions text</note>
|
<note from="description" priority="1">No playlist subscriptions text</note>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">app/subscriptions/subscriptions.component.html</context>
|
<context context-type="sourcefile">app/subscriptions/subscriptions.component.html</context>
|
||||||
@@ -2059,7 +2059,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="fb35145bfb84521e21b6385363d59221f436a573" datatype="html">
|
<trans-unit id="fb35145bfb84521e21b6385363d59221f436a573" datatype="html">
|
||||||
<source>Kill all downloads</source>
|
<source>Kill all downloads</source>
|
||||||
<target>Alle Herunterladen-Ereignisse abbrechen</target>
|
<target state="translated">Alle Downloads abbrechen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">app/settings/settings.component.html</context>
|
<context context-type="sourcefile">app/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">139</context>
|
<context context-type="linenumber">139</context>
|
||||||
@@ -2221,7 +2221,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3697f8583ea42868aa269489ad366103d94aece7" datatype="html">
|
<trans-unit id="3697f8583ea42868aa269489ad366103d94aece7" datatype="html">
|
||||||
<source>Editing</source>
|
<source>Editing</source>
|
||||||
<target>Bearbeiten</target>
|
<target state="translated">In Bearbeitung</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
|
<context context-type="sourcefile">app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
|
||||||
<context context-type="linenumber">1</context>
|
<context context-type="linenumber">1</context>
|
||||||
@@ -2592,7 +2592,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="d54142de169844b014ae913a4056c31495f4a305" datatype="html">
|
<trans-unit id="d54142de169844b014ae913a4056c31495f4a305" datatype="html">
|
||||||
<source>Test connection string</source>
|
<source>Test connection string</source>
|
||||||
<target>Verbindungstest-String</target>
|
<target state="translated">Verbindungsstring testen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">304</context>
|
<context context-type="linenumber">304</context>
|
||||||
@@ -2717,7 +2717,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="49e09cce4426975ba06c1667063d2c1df9c94362" datatype="html">
|
<trans-unit id="49e09cce4426975ba06c1667063d2c1df9c94362" datatype="html">
|
||||||
<source>Autoplay</source>
|
<source>Autoplay</source>
|
||||||
<target>Automatisches abspielen</target>
|
<target state="translated">Automatisches Abspielen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/main/main.component.html</context>
|
<context context-type="sourcefile">src/app/main/main.component.html</context>
|
||||||
<context context-type="linenumber">70,71</context>
|
<context context-type="linenumber">70,71</context>
|
||||||
@@ -4309,7 +4309,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6219551536751479443" datatype="html">
|
<trans-unit id="6219551536751479443" datatype="html">
|
||||||
<source>Finished downloading</source>
|
<source>Finished downloading</source>
|
||||||
<target state="translated">Herunterladen abgeschlossen</target>
|
<target state="translated">Download abgeschlossen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
||||||
<context context-type="linenumber">17</context>
|
<context context-type="linenumber">17</context>
|
||||||
@@ -4317,7 +4317,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5947241266456580665" datatype="html">
|
<trans-unit id="5947241266456580665" datatype="html">
|
||||||
<source>Download failed</source>
|
<source>Download failed</source>
|
||||||
<target state="translated">Herunterladen fehlgeschlagen</target>
|
<target state="translated">Download fehlgeschlagen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
||||||
<context context-type="linenumber">18</context>
|
<context context-type="linenumber">18</context>
|
||||||
@@ -4358,7 +4358,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6876310993601590130" datatype="html">
|
<trans-unit id="6876310993601590130" datatype="html">
|
||||||
<source>Download completed</source>
|
<source>Download completed</source>
|
||||||
<target state="translated">Herunterladen abgeschlossen</target>
|
<target state="translated">Download abgeschlossen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
|
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
|
||||||
<context context-type="linenumber">23</context>
|
<context context-type="linenumber">23</context>
|
||||||
@@ -4523,7 +4523,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="c5dc5fbcce45e9b1530e2a5c2baa8ebe722aef4c" datatype="html">
|
<trans-unit id="c5dc5fbcce45e9b1530e2a5c2baa8ebe722aef4c" datatype="html">
|
||||||
<source>Download complete</source>
|
<source>Download complete</source>
|
||||||
<target state="translated">Herunterladen abgeschlossen</target>
|
<target state="translated">Download abgeschlossen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">391</context>
|
<context context-type="linenumber">391</context>
|
||||||
@@ -4703,7 +4703,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8643601595923420698" datatype="html">
|
<trans-unit id="8643601595923420698" datatype="html">
|
||||||
<source>Retry download</source>
|
<source>Retry download</source>
|
||||||
<target state="translated">Herunterladen erneut versuchen</target>
|
<target state="translated">Download erneut versuchen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
||||||
<context context-type="linenumber">31</context>
|
<context context-type="linenumber">31</context>
|
||||||
@@ -4727,7 +4727,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5000203534763292992" datatype="html">
|
<trans-unit id="5000203534763292992" datatype="html">
|
||||||
<source>Download restarted!</source>
|
<source>Download restarted!</source>
|
||||||
<target state="translated">Herunterladen neu gestartet!</target>
|
<target state="translated">Download neu gestartet!</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
|
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
|
||||||
<context context-type="linenumber">72</context>
|
<context context-type="linenumber">72</context>
|
||||||
@@ -4783,7 +4783,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html">
|
<trans-unit id="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html">
|
||||||
<source>Download error</source>
|
<source>Download error</source>
|
||||||
<target state="translated">Herunterladefehler</target>
|
<target state="translated">Downloadfehler</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">392</context>
|
<context context-type="linenumber">392</context>
|
||||||
@@ -4941,6 +4941,206 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">No description</note>
|
<note priority="1" from="description">No description</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="4665451070906079743" datatype="html">
|
||||||
|
<source>Favorited</source>
|
||||||
|
<target state="translated">Favorisiert</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="56b1a3c93fb95fed1805005c561a5e431d57ffae" datatype="html">
|
||||||
|
<source>Blacklist all files</source>
|
||||||
|
<target state="translated">Alle Dateien blacklisten</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">Gelöschte Abo-Dateien blacklisten</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="c2faa86201eab08b5b39b5437f96ab9432e125e7" datatype="html">
|
||||||
|
<source>Item limit</source>
|
||||||
|
<target state="translated">Elementlimit</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="784837056777689544" datatype="html">
|
||||||
|
<source>Would you like to unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/>?</source>
|
||||||
|
<target state="translated">Möchtest Du dich von <x id="subscription name" equiv-text="this.sub['name']"/> abmelden?</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="d618f383a0ea2458eeb945a85190d4a002ea394b" datatype="html">
|
||||||
|
<source>Arg</source>
|
||||||
|
<target state="translated">Argument</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="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html">
|
||||||
|
<source>See docs here.</source>
|
||||||
|
<target state="translated">Doku ansehen.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">375</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">382</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">392</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">402</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">409</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Discord API setting hint</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9e766e11a9de375907aaf566897ecc6dac393ebc" datatype="html">
|
||||||
|
<source>Webhook URL</source>
|
||||||
|
<target state="translated">Webhook URL</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">366</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="b770c48628d98cb4633d6a17e3f0ba0265376af5" datatype="html">
|
||||||
|
<source>Gotify app token</source>
|
||||||
|
<target state="translated">Gotify app token</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">407</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Gotify app token</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3e420c675b8f3f3702576d52e8bb6e8e1d3feda0" datatype="html">
|
||||||
|
<source>How do I get the chat ID?</source>
|
||||||
|
<target state="translated">Wie bekomme ich die Chat ID?</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">Telegram chat ID help</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="06f503e492d6dbcf59e7b9c412ca86913d718689" datatype="html">
|
||||||
|
<source>ntfy topic URL</source>
|
||||||
|
<target state="translated">ntfy Themen URL</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">ntfy topic URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html">
|
||||||
|
<source>Use gotify API</source>
|
||||||
|
<target state="translated">gotify API verwenden</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">396</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">Gotify Server URL</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">400</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Gotify server URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="144e1a21ebe8fa238f88d2ac27515ed711cfc9a0" datatype="html">
|
||||||
|
<source>Telegram chat ID</source>
|
||||||
|
<target state="translated">Telegram chat ID</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">424</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Telegram chat ID</note>
|
||||||
|
</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">Möchtest du <x id="category name" equiv-text="category['name']"/> löschen?</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">159</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">Löschen von <x id="category name" equiv-text="category['name']"/> fehlgeschlagen!</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">172</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8336047719608684263" datatype="html">
|
||||||
|
<source>Unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/></source>
|
||||||
|
<target state="translated">Von <x id="subscription name" equiv-text="this.sub['name']"/> abmelden</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="37469c9f3e31d95087fa22b6c9c3bc64adf1692d" datatype="html">
|
||||||
|
<source>Enable RSS Feed</source>
|
||||||
|
<target state="translated">RSS Feed aktivieren</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">271</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Enable RSS Feed setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3264d82792954815be755b3da01e2625458711dc" datatype="html">
|
||||||
|
<source>Discord Webhook URL</source>
|
||||||
|
<target state="translated">Discord Webhook URL</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">373</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Discord Webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7cedb649779673568447b994463b2882c4e0436a" datatype="html">
|
||||||
|
<source>Slack Webhook URL</source>
|
||||||
|
<target state="translated">Slack Webhook URL</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">380</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Slack Webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8c1bf02206fbc371ff69ab1b7e35a17ba29d169d" datatype="html">
|
||||||
|
<source>Use ntfy API</source>
|
||||||
|
<target state="translated">ntfy API verwenden</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">386</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Use ntfy API setting</note>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -4802,7 +4802,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2481374649045841364" datatype="html">
|
<trans-unit id="2481374649045841364" datatype="html">
|
||||||
<source>Would you like to delete <x id="category name" equiv-text="category['name']"/>?</source>
|
<source>Would you like to delete <x id="category name" equiv-text="category['name']"/>?</source>
|
||||||
<target state="needs-translation">Apakah Anda ingin menghapus <x id="category name" equiv-text="category['name']"/>?</target>
|
<target state="translated">Apakah Anda ingin menghapus <x id="category name" equiv-text="category['name']"/>?</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">159</context>
|
<context context-type="linenumber">159</context>
|
||||||
@@ -5026,6 +5026,70 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">Telegram chat ID</note>
|
<note priority="1" from="description">Telegram chat ID</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="af30e51aa8b67e1133a341ec28359be05150e65c" datatype="html">
|
||||||
|
<source>No description available.</source>
|
||||||
|
<target state="translated">Tidak ada deskripsi.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/player/player.component.html</context>
|
||||||
|
<context context-type="linenumber">25,27</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">No description</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="e0a11fbea353b1ce1131161774e4a3e10bcb99b1" datatype="html">
|
||||||
|
<source>Large</source>
|
||||||
|
<target state="translated">Besar</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">44,46</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Large</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="b6399391e706e2d7b7b7880eb5630e4e6f49728c" datatype="html">
|
||||||
|
<source>Side</source>
|
||||||
|
<target state="translated">Samping</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">35,37</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Side</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="1236604279860679031" datatype="html">
|
||||||
|
<source>Restart</source>
|
||||||
|
<target state="translated">Mulai ulang</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">62</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9042260521669277115" datatype="html">
|
||||||
|
<source>Pause</source>
|
||||||
|
<target state="translated">Jeda</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">68</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7182974689040833178" datatype="html">
|
||||||
|
<source>Resume</source>
|
||||||
|
<target state="translated">Lanjutkan</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">74</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">80</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="338b44701a53ce3ef2f36abfb56f89c3edfa9eab" datatype="html">
|
||||||
|
<source>Over</source>
|
||||||
|
<target state="translated">Lebih</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">32,34</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Over</note>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -4119,6 +4119,963 @@
|
|||||||
<context context-type="linenumber">58</context>
|
<context context-type="linenumber">58</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="ea2b65121b93921fe54692025da9b9e3ce779ad5" datatype="html">
|
||||||
|
<source>Task settings - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></source>
|
||||||
|
<target state="translated">Impostazioni attività - <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="3264d82792954815be755b3da01e2625458711dc" datatype="html">
|
||||||
|
<source>Discord Webhook URL</source>
|
||||||
|
<target state="translated">URL webhook Discord</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">373</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Discord Webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html">
|
||||||
|
<source>Use gotify API</source>
|
||||||
|
<target state="translated">Utilizza API gotify</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">396</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Use gotify API setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5000203534763292992" datatype="html">
|
||||||
|
<source>Download restarted!</source>
|
||||||
|
<target state="translated">Download riavviato!</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="8643601595923420698" datatype="html">
|
||||||
|
<source>Retry download</source>
|
||||||
|
<target state="translated">Riprova download</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="6785427850041119037" datatype="html">
|
||||||
|
<source>Delete category</source>
|
||||||
|
<target state="translated">Elimina categoria</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">158</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html">
|
||||||
|
<source>Download error</source>
|
||||||
|
<target state="translated">Errore nel download</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">359</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Download error</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9176960997786930103" datatype="html">
|
||||||
|
<source>Error for: <x id="PH" equiv-text="task['title']"/></source>
|
||||||
|
<target state="translated">Errore per: <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="019d4bd6a5690f0cfa0ecf346a4e6bf7f0d8debb" datatype="html">
|
||||||
|
<source>Remove</source>
|
||||||
|
<target state="translated">Rimuovi</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="b770c48628d98cb4633d6a17e3f0ba0265376af5" datatype="html">
|
||||||
|
<source>Gotify app token</source>
|
||||||
|
<target state="translated">Token app Gotify</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">407</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Gotify app token</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="56b1a3c93fb95fed1805005c561a5e431d57ffae" datatype="html">
|
||||||
|
<source>Blacklist all files</source>
|
||||||
|
<target state="translated">Metti nella lista nera tutti i file</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="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="1d4fa01d25990f60abf21c3a451fa8ba262b7912" datatype="html">
|
||||||
|
<source>Unfavorite</source>
|
||||||
|
<target state="translated">Non preferito</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="1f2809e6a99d511fdb6eaf041d785fe54d0680cc" datatype="html">
|
||||||
|
<source>File card size</source>
|
||||||
|
<target state="translated">Dimensione carta file</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">42</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">File card size</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="28da11220a3377ddce3c7948825d33101f142782" datatype="html">
|
||||||
|
<source>Extractor</source>
|
||||||
|
<target state="translated">Estrattore</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="338b44701a53ce3ef2f36abfb56f89c3edfa9eab" datatype="html">
|
||||||
|
<source>Over</source>
|
||||||
|
<target state="translated">Sopra</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">32,34</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Over</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="378c072ce05889c9771718d05106e7685fcd3507" datatype="html">
|
||||||
|
<source>Medium</source>
|
||||||
|
<target state="translated">Media</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">47,49</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Medium</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9a865c2922f5c01899d06c472dba2e5bd63bcff9" datatype="html">
|
||||||
|
<source>Small</source>
|
||||||
|
<target state="translated">Piccola</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">50,52</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Small</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5a105e7bd7e7db6ea211fe950fc9f317379acceb" datatype="html">
|
||||||
|
<source>No notifications available</source>
|
||||||
|
<target state="translated">Nessuna notifica disponibile</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="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html">
|
||||||
|
<source>See docs here.</source>
|
||||||
|
<target state="translated">Vedi la documentazione qui.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">375</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">382</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">392</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">402</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">409</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Discord API setting hint</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8c1bf02206fbc371ff69ab1b7e35a17ba29d169d" datatype="html">
|
||||||
|
<source>Use ntfy API</source>
|
||||||
|
<target state="translated">Utilizza API ntfy</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">386</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Use ntfy API setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9aa62bf1a535a97a4d752bbc5cf1c31af0f0c1f7" datatype="html">
|
||||||
|
<source>Supports regex</source>
|
||||||
|
<target state="translated">Supporta espressioni regolari</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="4665451070906079743" datatype="html">
|
||||||
|
<source>Favorited</source>
|
||||||
|
<target state="translated">Preferito</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="7cedb649779673568447b994463b2882c4e0436a" datatype="html">
|
||||||
|
<source>Slack Webhook URL</source>
|
||||||
|
<target state="translated">URL webhook Slack</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">380</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Slack Webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="b6399391e706e2d7b7b7880eb5630e4e6f49728c" datatype="html">
|
||||||
|
<source>Side</source>
|
||||||
|
<target state="translated">Laterale</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">35,37</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Side</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="144e1a21ebe8fa238f88d2ac27515ed711cfc9a0" datatype="html">
|
||||||
|
<source>Telegram chat ID</source>
|
||||||
|
<target state="translated">ID chat Telegram</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">424</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Telegram chat ID</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="c150a30bbbdb175b4d08820196a9acb66b167653" datatype="html">
|
||||||
|
<source>Archives empty</source>
|
||||||
|
<target state="translated">Archivi vuoti</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="5947241266456580665" datatype="html">
|
||||||
|
<source>Download failed</source>
|
||||||
|
<target state="translated">Download non riuscito</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="b78a98bc54259a29cf6250dbaeab5fe11fae91cf" datatype="html">
|
||||||
|
<source>Favorited</source>
|
||||||
|
<target state="translated">Preferito</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="51a161ce175abcd44f6c6cbd0e996681bf553ac3" datatype="html">
|
||||||
|
<source>Delete selected</source>
|
||||||
|
<target state="translated">Cancella selezionati</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="35cf4cdcedc8ef3f94b6100e0d86836e31dbb908" datatype="html">
|
||||||
|
<source>Force autoplay</source>
|
||||||
|
<target state="translated">Forza l'auto-riproduzione</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">218</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Force autoplay setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3640026747176198246" datatype="html">
|
||||||
|
<source>Watch content</source>
|
||||||
|
<target state="translated">Guarda contenuto</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">50</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
|
||||||
|
<source>None</source>
|
||||||
|
<target state="translated">Nessuno</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="06f503e492d6dbcf59e7b9c412ca86913d718689" datatype="html">
|
||||||
|
<source>ntfy topic URL</source>
|
||||||
|
<target state="translated">URL argomento ntfy</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">ntfy topic URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
|
||||||
|
<source>User</source>
|
||||||
|
<target state="translated">Utente</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="c41475a25c9f9d9639db9efa56637603a77528b4" datatype="html">
|
||||||
|
<source>Download archive</source>
|
||||||
|
<target state="translated">Scarica archivio</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="8224301330941792118" datatype="html">
|
||||||
|
<source>Failed to delete archive items!</source>
|
||||||
|
<target state="translated">Impossibile eliminare l'archivio/i!</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="33a7c6d5ff3515fa237f1fd4e43df8b65373954d" datatype="html">
|
||||||
|
<source>Enable all notifications</source>
|
||||||
|
<target state="translated">Abilita tutte le notifiche</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">352</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Enable all notifications setting</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">Desideri eliminare <x id="selected archives amount" equiv-text="this.selection.selected.length"/> archivio/i?</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="11a0771f88158a540a54e0e4ec5d25733d65fc0e" datatype="html">
|
||||||
|
<source>Favorite</source>
|
||||||
|
<target state="translated">Preferito</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="2525880134753073592" datatype="html">
|
||||||
|
<source>Successfully deleted archive items!</source>
|
||||||
|
<target state="translated">Archivio/i eliminato/i con successo!</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="c65dd978b3c7566551c0ebefb234c2d41942b847" datatype="html">
|
||||||
|
<source>Delete files older than</source>
|
||||||
|
<target state="translated">Elimina file più vecchi di</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="1698114086921246480" datatype="html">
|
||||||
|
<source>Unsubscribe</source>
|
||||||
|
<target state="translated">Annulla iscrizione</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="d57c023a4cf63b2f12c10328c15b636ff18929aa" datatype="html">
|
||||||
|
<source>Best</source>
|
||||||
|
<target state="translated">Migliore</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="c5dc5fbcce45e9b1530e2a5c2baa8ebe722aef4c" datatype="html">
|
||||||
|
<source>Download complete</source>
|
||||||
|
<target state="translated">Download completato</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">358</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Download complete</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5709555629190115111" datatype="html">
|
||||||
|
<source>View task</source>
|
||||||
|
<target state="translated">Visualizza attività</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="7410432243549869948" datatype="html">
|
||||||
|
<source>Duration</source>
|
||||||
|
<target state="translated">Durata</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="7332320960988475089" datatype="html">
|
||||||
|
<source>Successfully deleted <x id="category name" equiv-text="category['name']"/>!</source>
|
||||||
|
<target state="translated">Categoria <x id="category name" equiv-text="category['name']"/> eliminata con successo!</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">168</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8336047719608684263" datatype="html">
|
||||||
|
<source>Unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/></source>
|
||||||
|
<target state="translated">Annulla l'iscrizione da <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="3371159074051387771" datatype="html">
|
||||||
|
<source>Failed to delete <x id="category name" equiv-text="category['name']"/>!</source>
|
||||||
|
<target state="translated">Impossibile eliminare la categoria <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">172</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html">
|
||||||
|
<source>Archives</source>
|
||||||
|
<target state="translated">Archivi</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="8443034725057696949" datatype="html">
|
||||||
|
<source>Task finished</source>
|
||||||
|
<target state="translated">Attività completata</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="2361a4f76caaa4574803fbcdca8b0a47c91cc7ed" datatype="html">
|
||||||
|
<source>Task finished</source>
|
||||||
|
<target state="translated">Attività completata</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">360</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Task finished</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="1a9b415816364f554ee411020e65219092655271" datatype="html">
|
||||||
|
<source>Title filter</source>
|
||||||
|
<target state="translated">Filtro titolo</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="e0a11fbea353b1ce1131161774e4a3e10bcb99b1" datatype="html">
|
||||||
|
<source>Large</source>
|
||||||
|
<target state="translated">Grande</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">44,46</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Large</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="674a999dd48d7da565ffdd105602261b8a4761ea" datatype="html">
|
||||||
|
<source>Download zip</source>
|
||||||
|
<target state="translated">Scarica 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="55f559d6f666b945479f534b0c182f70cd0a8a69" datatype="html">
|
||||||
|
<source>Gotify server URL</source>
|
||||||
|
<target state="translated">URL server Gotify</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">400</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Gotify server URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9c562d26e041390ecc3f49dabc51cc50ebba7469" datatype="html">
|
||||||
|
<source>Allowed notification types</source>
|
||||||
|
<target state="translated">Tipi di notifiche consentiti</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">356</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Allowed notification types</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7b4585a9072f3c1292972c14a3d0e14978fbfc9c" datatype="html">
|
||||||
|
<source>Delete old files:</source>
|
||||||
|
<target state="translated">Elimina vecchi file:</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="6219551536751479443" datatype="html">
|
||||||
|
<source>Finished downloading</source>
|
||||||
|
<target state="translated">Download completato</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="6268070779441507380" datatype="html">
|
||||||
|
<source>Download Date</source>
|
||||||
|
<target state="translated">Data download</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="2481374649045841364" datatype="html">
|
||||||
|
<source>Would you like to delete <x id="category name" equiv-text="category['name']"/>?</source>
|
||||||
|
<target state="translated">Desideri eliminare <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">159</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="eeb0ba2e4743901d8f5eebd9a3529aa1f236c608" datatype="html">
|
||||||
|
<source>Create bot here.</source>
|
||||||
|
<target state="translated">Crea il bot qui.</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">Telegram bot create link</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6437411876967154040" datatype="html">
|
||||||
|
<source>Audio only</source>
|
||||||
|
<target state="translated">Solo 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="6080b77234e92ad41bb52653b239c4c4f851317d" datatype="html">
|
||||||
|
<source>Error</source>
|
||||||
|
<target state="translated">Errore</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
|
||||||
|
<context context-type="linenumber">39</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Error</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8456659390937171831" datatype="html">
|
||||||
|
<source>Show error</source>
|
||||||
|
<target state="translated">Mostra errore</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">56</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="4b3972c3e9485218508a95f7a4ce7758e3f09ced" datatype="html">
|
||||||
|
<source>Upload</source>
|
||||||
|
<target state="translated">Carica</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="37469c9f3e31d95087fa22b6c9c3bc64adf1692d" datatype="html">
|
||||||
|
<source>Enable RSS Feed</source>
|
||||||
|
<target state="translated">Abilita feed RSS</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">271</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Enable RSS Feed setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="1236604279860679031" datatype="html">
|
||||||
|
<source>Restart</source>
|
||||||
|
<target state="translated">Riavvia</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">62</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8bcabdf6b16cad0313a86c7e940c5e3ad7f9f8ab" datatype="html">
|
||||||
|
<source>Notifications</source>
|
||||||
|
<target state="translated">Notifiche</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">343</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Notifications settings label</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">Desideri annullare l'iscrizione da <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="1091872159779006651" datatype="html">
|
||||||
|
<source>You must input a time!</source>
|
||||||
|
<target state="translated">Devi inserire un tempo!</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="0ed98b4c6ec1db6708a963e8a2699478ac97f55c" datatype="html">
|
||||||
|
<source>Add subscription</source>
|
||||||
|
<target state="translated">Aggiungi abbonamento</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="61d6b5fa4311b1c617b66dad72496f9dd43b07b4" datatype="html">
|
||||||
|
<source>Be careful enabling this with multi-user mode! User data may be exposed.</source>
|
||||||
|
<target state="translated">Attenzione all'abilitazione in modalità multiutente! I dati dell'utente potrebbero essere esposti.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">272</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">RSS Feed prefix</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3533826530554274875" datatype="html">
|
||||||
|
<source>Upload Date</source>
|
||||||
|
<target state="translated">Data upload</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="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="cdf5297d8d080a78e8b10debc5c38b7845a3cbe7" datatype="html">
|
||||||
|
<source>Do not ask for confirmation</source>
|
||||||
|
<target state="translated">Non chiedere conferma</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="c2faa86201eab08b5b39b5437f96ab9432e125e7" datatype="html">
|
||||||
|
<source>Item limit</source>
|
||||||
|
<target state="translated">Limite elementi</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="7022070615528435141" datatype="html">
|
||||||
|
<source>Delete</source>
|
||||||
|
<target state="translated">Elimina</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">160</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9e766e11a9de375907aaf566897ecc6dac393ebc" datatype="html">
|
||||||
|
<source>Webhook URL</source>
|
||||||
|
<target state="translated">URL webhook</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">366</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2e076ff9866213d0815961c494aa48b177046b9d" datatype="html">
|
||||||
|
<source>Telegram bot token</source>
|
||||||
|
<target state="translated">Token bot Telegram</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">417</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Telegram bot token</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="d618f383a0ea2458eeb945a85190d4a002ea394b" datatype="html">
|
||||||
|
<source>Arg</source>
|
||||||
|
<target state="translated">Argomento</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="1879058637439215882" datatype="html">
|
||||||
|
<source>Download error</source>
|
||||||
|
<target state="translated">Errore nel download</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="8953483585652369683" datatype="html">
|
||||||
|
<source>Archive successfully imported!</source>
|
||||||
|
<target state="translated">Archivio importato con successo!</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="7911845622864460134" datatype="html">
|
||||||
|
<source>Video only</source>
|
||||||
|
<target state="translated">Solo 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="38992954440d6afb54aeb58af12ca0123ee5e26e" datatype="html">
|
||||||
|
<source>Use Telegram API</source>
|
||||||
|
<target state="translated">Utilizza API Telegram</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">413</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Use Telegram API setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6876310993601590130" datatype="html">
|
||||||
|
<source>Download completed</source>
|
||||||
|
<target state="translated">Download completato</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">Attività</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="5ac5a0e5ffe8e5623b40696f4c2403c17349271f" datatype="html">
|
||||||
|
<source>Sidepanel mode</source>
|
||||||
|
<target state="translated">Modalità pannello laterale</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">30</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Sidepanel mode</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8571838164752006148" datatype="html">
|
||||||
|
<source>View error</source>
|
||||||
|
<target state="translated">Visualizza errore</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="9aa1b4779a515170b297d2c0507e6ff9d2e3e0e0" datatype="html">
|
||||||
|
<source>Blacklist deleted subscription files</source>
|
||||||
|
<target state="translated">Metti nella lista nera i file di abbonamenti eliminati</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="9042260521669277115" datatype="html">
|
||||||
|
<source>Pause</source>
|
||||||
|
<target state="translated">Metti in pausa</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">68</context>
|
||||||
|
</context-group>
|
||||||
|
</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="8c6e24eab969d9f63a8a0e9d617aee3b99e28ae6" datatype="html">
|
||||||
|
<source>Play all</source>
|
||||||
|
<target state="translated">Riproduci tutto</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="3e420c675b8f3f3702576d52e8bb6e8e1d3feda0" datatype="html">
|
||||||
|
<source>How do I get the chat ID?</source>
|
||||||
|
<target state="translated">Come posso ottenere l'ID chat?</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">Telegram chat ID help</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7182974689040833178" datatype="html">
|
||||||
|
<source>Resume</source>
|
||||||
|
<target state="translated">Riprendi</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">74</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">80</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8564202903947049539" datatype="html">
|
||||||
|
<source>Play</source>
|
||||||
|
<target state="translated">Riproduci</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="a4ed8eba1e057e67d5c2d87b52230f182b3dae4e" datatype="html">
|
||||||
|
<source>Restart required.</source>
|
||||||
|
<target state="translated">Riavvio richiesto.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">446</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Restart required hint</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3159807825117518005" datatype="html">
|
||||||
|
<source>Delete archives</source>
|
||||||
|
<target state="translated">Elimina archivi</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="c35ef0f03a863d33b04aae6807f140397a50f491" datatype="html">
|
||||||
|
<source>Generate RSS URL</source>
|
||||||
|
<target state="translated">Genera URL 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">273</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Generate RSS URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8953033926734869941" datatype="html">
|
||||||
|
<source>Name</source>
|
||||||
|
<target state="translated">Nome</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="5ca707824ab93066c7d9b44e1b8bf216725c2c22" datatype="html">
|
||||||
|
<source>Filter</source>
|
||||||
|
<target state="translated">Filtra</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="c40370dc182b5e4333828b70f7478bde58bb5cfe" datatype="html">
|
||||||
|
<source>Enable notifications</source>
|
||||||
|
<target state="translated">Abilita notifiche</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">349</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Enable notifications setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2492098975665776610" datatype="html">
|
||||||
|
<source>File Size</source>
|
||||||
|
<target state="translated">Dimensione file</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="af30e51aa8b67e1133a341ec28359be05150e65c" datatype="html">
|
||||||
|
<source>No description available.</source>
|
||||||
|
<target state="translated">Nessuna descrizione disponibile.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/player/player.component.html</context>
|
||||||
|
<context context-type="linenumber">25,27</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">No description</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="fd467148a18f0921c10d116d4e0174fe29452be4" datatype="html">
|
||||||
|
<source>See documentation here.</source>
|
||||||
|
<target state="translated">Vedi la documentazione qui.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">274</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">RSS feed documentation</note>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4142,6 +4142,963 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">Video resolution property</note>
|
<note priority="1" from="description">Video resolution property</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
|
||||||
|
<source>None</source>
|
||||||
|
<target state="translated">Geen</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="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="6080b77234e92ad41bb52653b239c4c4f851317d" datatype="html">
|
||||||
|
<source>Error</source>
|
||||||
|
<target state="translated">Fout</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
|
||||||
|
<context context-type="linenumber">39</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Error</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3640026747176198246" datatype="html">
|
||||||
|
<source>Watch content</source>
|
||||||
|
<target state="translated">Inhoud bekijken</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">50</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="c150a30bbbdb175b4d08820196a9acb66b167653" datatype="html">
|
||||||
|
<source>Archives empty</source>
|
||||||
|
<target state="translated">Archieven leeg</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="8571838164752006148" datatype="html">
|
||||||
|
<source>View error</source>
|
||||||
|
<target state="translated">Toon foutmelding</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">Toon taak</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="b78a98bc54259a29cf6250dbaeab5fe11fae91cf" datatype="html">
|
||||||
|
<source>Favorited</source>
|
||||||
|
<target state="translated">Favorieten</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="5ac5a0e5ffe8e5623b40696f4c2403c17349271f" datatype="html">
|
||||||
|
<source>Sidepanel mode</source>
|
||||||
|
<target state="translated">Zijpaneel-modus</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">30</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Sidepanel mode</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8c1bf02206fbc371ff69ab1b7e35a17ba29d169d" datatype="html">
|
||||||
|
<source>Use ntfy API</source>
|
||||||
|
<target state="translated">Gebruik ntfy API</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">386</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Use ntfy API setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="0ed98b4c6ec1db6708a963e8a2699478ac97f55c" datatype="html">
|
||||||
|
<source>Add subscription</source>
|
||||||
|
<target state="translated">Abonnement toevoegen</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="9a865c2922f5c01899d06c472dba2e5bd63bcff9" datatype="html">
|
||||||
|
<source>Small</source>
|
||||||
|
<target state="translated">Klein</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">50,52</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Small</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html">
|
||||||
|
<source>Archives</source>
|
||||||
|
<target state="translated">Archieven</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="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">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="51a161ce175abcd44f6c6cbd0e996681bf553ac3" datatype="html">
|
||||||
|
<source>Delete selected</source>
|
||||||
|
<target state="translated">Verwijder geselecteerde</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">Archief downloaden</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="4b3972c3e9485218508a95f7a4ce7758e3f09ced" datatype="html">
|
||||||
|
<source>Upload</source>
|
||||||
|
<target state="translated">Uploaden</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">Archief succesvol geïmporteerd!</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">Archieven verwijderen</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">Wilt u <x id="selected archives amount" equiv-text="this.selection.selected.length"/> archieven verwijderen?</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="7022070615528435141" datatype="html">
|
||||||
|
<source>Delete</source>
|
||||||
|
<target state="translated">Verwijderen</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">160</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2525880134753073592" datatype="html">
|
||||||
|
<source>Successfully deleted archive items!</source>
|
||||||
|
<target state="translated">Archiefstukken succesvol verwijderd!</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">Archiefstukken verwijderen mislukt!</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="8443034725057696949" datatype="html">
|
||||||
|
<source>Task finished</source>
|
||||||
|
<target state="translated">Taak voltooid</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="6219551536751479443" datatype="html">
|
||||||
|
<source>Finished downloading</source>
|
||||||
|
<target state="translated">Downloaden voltooid</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">Download mislukt</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="8564202903947049539" datatype="html">
|
||||||
|
<source>Play</source>
|
||||||
|
<target state="translated">Afspelen</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">Download opnieuw proberen</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="1879058637439215882" datatype="html">
|
||||||
|
<source>Download error</source>
|
||||||
|
<target state="translated">Download fout</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">Taak</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="5a105e7bd7e7db6ea211fe950fc9f317379acceb" datatype="html">
|
||||||
|
<source>No notifications available</source>
|
||||||
|
<target state="translated">Geen notificaties beschikbaar</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">Download voltooid</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">Alleen 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="7911845622864460134" datatype="html">
|
||||||
|
<source>Video only</source>
|
||||||
|
<target state="translated">Alleen 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="4665451070906079743" datatype="html">
|
||||||
|
<source>Favorited</source>
|
||||||
|
<target state="translated">Favoriet</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">Downloaddatum</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">Naam</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">Bestandsgrootte</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">Duur</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">Verwijder bestanden ouder dan</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="3533826530554274875" datatype="html">
|
||||||
|
<source>Upload Date</source>
|
||||||
|
<target state="translated">Uploaddatum</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="9aa1b4779a515170b297d2c0507e6ff9d2e3e0e0" datatype="html">
|
||||||
|
<source>Blacklist deleted subscription files</source>
|
||||||
|
<target state="translated">Verwijderde abonnementsbestanden op de zwarte lijst zetten</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">Niet om bevestiging vragen</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="56b1a3c93fb95fed1805005c561a5e431d57ffae" datatype="html">
|
||||||
|
<source>Blacklist all files</source>
|
||||||
|
<target state="translated">Alle bestanden op de zwarte lijst zetten</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="9176960997786930103" datatype="html">
|
||||||
|
<source>Error for: <x id="PH" equiv-text="task['title']"/></source>
|
||||||
|
<target state="translated">Fout voor: <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">Favoriet</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">Verwijder uit favorieten</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="1f2809e6a99d511fdb6eaf041d785fe54d0680cc" datatype="html">
|
||||||
|
<source>File card size</source>
|
||||||
|
<target state="translated">Bestandskaart grootte</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">42</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">Optie</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="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
|
||||||
|
<source>User</source>
|
||||||
|
<target state="translated">Gebruiker</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="c35ef0f03a863d33b04aae6807f140397a50f491" datatype="html">
|
||||||
|
<source>Generate RSS URL</source>
|
||||||
|
<target state="translated">RSS-URL genereren</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">273</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Generate RSS URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="35cf4cdcedc8ef3f94b6100e0d86836e31dbb908" datatype="html">
|
||||||
|
<source>Force autoplay</source>
|
||||||
|
<target state="translated">Automatisch afspelen forceren</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">218</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Force autoplay 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">Wees voorzichtig als je dit aanzet samen met de modus voor meerdere gebruikers! Gebruikersdata kunnen blootgesteld worden.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">272</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">Zie documentatie hier.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">274</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">RSS feed documentation</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9c562d26e041390ecc3f49dabc51cc50ebba7469" datatype="html">
|
||||||
|
<source>Allowed notification types</source>
|
||||||
|
<target state="translated">Toegestane typen notificaties</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">356</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Allowed notification types</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="33a7c6d5ff3515fa237f1fd4e43df8b65373954d" datatype="html">
|
||||||
|
<source>Enable all notifications</source>
|
||||||
|
<target state="translated">Alle notificaties inschakelen</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">352</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Enable all notifications setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9e766e11a9de375907aaf566897ecc6dac393ebc" datatype="html">
|
||||||
|
<source>Webhook URL</source>
|
||||||
|
<target state="translated">Webhook URL</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">366</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="55f559d6f666b945479f534b0c182f70cd0a8a69" datatype="html">
|
||||||
|
<source>Gotify server URL</source>
|
||||||
|
<target state="translated">Gotify server URL</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">400</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Gotify server URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="eeb0ba2e4743901d8f5eebd9a3529aa1f236c608" datatype="html">
|
||||||
|
<source>Create bot here.</source>
|
||||||
|
<target state="translated">Bot hier maken.</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">Telegram bot create link</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8c6e24eab969d9f63a8a0e9d617aee3b99e28ae6" datatype="html">
|
||||||
|
<source>Play all</source>
|
||||||
|
<target state="translated">Alles afspelen</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="7cedb649779673568447b994463b2882c4e0436a" datatype="html">
|
||||||
|
<source>Slack Webhook URL</source>
|
||||||
|
<target state="translated">Slack Webhook URL</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">380</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Slack Webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="a4ed8eba1e057e67d5c2d87b52230f182b3dae4e" datatype="html">
|
||||||
|
<source>Restart required.</source>
|
||||||
|
<target state="translated">Opnieuw starten vereist.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">446</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Restart required hint</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3e420c675b8f3f3702576d52e8bb6e8e1d3feda0" datatype="html">
|
||||||
|
<source>How do I get the chat ID?</source>
|
||||||
|
<target state="translated">Hoe kan ik het chat ID krijgen?</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">Telegram chat ID help</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8456659390937171831" datatype="html">
|
||||||
|
<source>Show error</source>
|
||||||
|
<target state="translated">Toon fout</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">56</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="1236604279860679031" datatype="html">
|
||||||
|
<source>Restart</source>
|
||||||
|
<target state="translated">Herstarten</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">62</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9042260521669277115" datatype="html">
|
||||||
|
<source>Pause</source>
|
||||||
|
<target state="translated">Pauzeren</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">68</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7182974689040833178" datatype="html">
|
||||||
|
<source>Resume</source>
|
||||||
|
<target state="translated">Hervatten</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">74</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">80</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="019d4bd6a5690f0cfa0ecf346a4e6bf7f0d8debb" datatype="html">
|
||||||
|
<source>Remove</source>
|
||||||
|
<target state="translated">Verwijderen</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="5000203534763292992" datatype="html">
|
||||||
|
<source>Download restarted!</source>
|
||||||
|
<target state="translated">Download herstart!</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="ea2b65121b93921fe54692025da9b9e3ce779ad5" datatype="html">
|
||||||
|
<source>Task settings - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></source>
|
||||||
|
<target state="translated">Taakinstellingen - <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="7b4585a9072f3c1292972c14a3d0e14978fbfc9c" datatype="html">
|
||||||
|
<source>Delete old files:</source>
|
||||||
|
<target state="translated">Verwijder oude bestanden:</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="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">Ondersteunt regex</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="c2faa86201eab08b5b39b5437f96ab9432e125e7" datatype="html">
|
||||||
|
<source>Item limit</source>
|
||||||
|
<target state="translated">Item limiet</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="8336047719608684263" datatype="html">
|
||||||
|
<source>Unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/></source>
|
||||||
|
<target state="translated">Afmelden van <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">Wil je afmelden van <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">Afmelden</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">Je moet een tijd invoeren!</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="338b44701a53ce3ef2f36abfb56f89c3edfa9eab" datatype="html">
|
||||||
|
<source>Over</source>
|
||||||
|
<target state="translated">Over</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">32,34</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Over</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="b6399391e706e2d7b7b7880eb5630e4e6f49728c" datatype="html">
|
||||||
|
<source>Side</source>
|
||||||
|
<target state="translated">Zijkant</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">35,37</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Side</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="e0a11fbea353b1ce1131161774e4a3e10bcb99b1" datatype="html">
|
||||||
|
<source>Large</source>
|
||||||
|
<target state="translated">Groot</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">44,46</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Large</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="af30e51aa8b67e1133a341ec28359be05150e65c" datatype="html">
|
||||||
|
<source>No description available.</source>
|
||||||
|
<target state="translated">Geen beschrijving beschikbaar.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/player/player.component.html</context>
|
||||||
|
<context context-type="linenumber">25,27</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">No description</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="c40370dc182b5e4333828b70f7478bde58bb5cfe" datatype="html">
|
||||||
|
<source>Enable notifications</source>
|
||||||
|
<target state="translated">Notificaties inschakelen</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">349</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Enable notifications setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8bcabdf6b16cad0313a86c7e940c5e3ad7f9f8ab" datatype="html">
|
||||||
|
<source>Notifications</source>
|
||||||
|
<target state="translated">Notificaties</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">343</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Notifications settings label</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="c5dc5fbcce45e9b1530e2a5c2baa8ebe722aef4c" datatype="html">
|
||||||
|
<source>Download complete</source>
|
||||||
|
<target state="translated">Download voltooid</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">358</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">Downloadfout</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">359</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">Taak voltooid</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">360</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Task finished</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3264d82792954815be755b3da01e2625458711dc" datatype="html">
|
||||||
|
<source>Discord Webhook URL</source>
|
||||||
|
<target state="translated">Discord Webhook URL</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">373</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Discord Webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html">
|
||||||
|
<source>See docs here.</source>
|
||||||
|
<target state="translated">Zie documentatie hier.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">375</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">382</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">392</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">402</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">409</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Discord API setting hint</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="06f503e492d6dbcf59e7b9c412ca86913d718689" datatype="html">
|
||||||
|
<source>ntfy topic URL</source>
|
||||||
|
<target state="translated">ntfy onderwerp URL</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">ntfy topic URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html">
|
||||||
|
<source>Use gotify API</source>
|
||||||
|
<target state="translated">Gebruik gotify API</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">396</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Use gotify API setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="b770c48628d98cb4633d6a17e3f0ba0265376af5" datatype="html">
|
||||||
|
<source>Gotify app token</source>
|
||||||
|
<target state="translated">Gotify app token</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">407</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 gebruiken</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">413</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 bot token</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">417</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Telegram bot token</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="144e1a21ebe8fa238f88d2ac27515ed711cfc9a0" datatype="html">
|
||||||
|
<source>Telegram chat ID</source>
|
||||||
|
<target state="translated">Telegram chat ID</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">424</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Telegram chat ID</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6785427850041119037" datatype="html">
|
||||||
|
<source>Delete category</source>
|
||||||
|
<target state="translated">Categorie verwijderen</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">158</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">Wil je <x id="category name" equiv-text="category['name']"/> verwijderen?</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">159</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']"/> is succesvol verwijderd!</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">168</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']"/> verwijderen mislukt!</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">172</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="674a999dd48d7da565ffdd105602261b8a4761ea" datatype="html">
|
||||||
|
<source>Download zip</source>
|
||||||
|
<target state="translated">Zip downloaden</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="378c072ce05889c9771718d05106e7685fcd3507" datatype="html">
|
||||||
|
<source>Medium</source>
|
||||||
|
<target state="translated">Gemiddeld</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">47,49</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Medium</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="37469c9f3e31d95087fa22b6c9c3bc64adf1692d" datatype="html">
|
||||||
|
<source>Enable RSS Feed</source>
|
||||||
|
<target state="translated">RSS-feed aanzetten</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">271</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Enable RSS Feed setting</note>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -3784,6 +3784,727 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">Download error</note>
|
<note priority="1" from="description">Download error</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="51a161ce175abcd44f6c6cbd0e996681bf553ac3" datatype="html">
|
||||||
|
<source>Delete selected</source>
|
||||||
|
<target state="translated">Usuń wybrane</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="7022070615528435141" datatype="html">
|
||||||
|
<source>Delete</source>
|
||||||
|
<target state="translated">Usuń</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">160</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5709555629190115111" datatype="html">
|
||||||
|
<source>View task</source>
|
||||||
|
<target state="translated">Wyświetl zadanie</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="4578192247039196794" datatype="html">
|
||||||
|
<source>Task</source>
|
||||||
|
<target state="translated">Zadanie</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="9a865c2922f5c01899d06c472dba2e5bd63bcff9" datatype="html">
|
||||||
|
<source>Small</source>
|
||||||
|
<target state="translated">Mały</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">50,52</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Small</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9c562d26e041390ecc3f49dabc51cc50ebba7469" datatype="html">
|
||||||
|
<source>Allowed notification types</source>
|
||||||
|
<target state="translated">Dozwolone typy powiadomień</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">356</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Allowed notification types</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="33a7c6d5ff3515fa237f1fd4e43df8b65373954d" datatype="html">
|
||||||
|
<source>Enable all notifications</source>
|
||||||
|
<target state="translated">Włącz wszystkie powiadomienia</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">352</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Enable all notifications setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6268070779441507380" datatype="html">
|
||||||
|
<source>Download Date</source>
|
||||||
|
<target state="translated">Data pobrania</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="9aa1b4779a515170b297d2c0507e6ff9d2e3e0e0" datatype="html">
|
||||||
|
<source>Blacklist deleted subscription files</source>
|
||||||
|
<target state="translated">Czarna lista usuniętych plików subskrypcji</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="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html">
|
||||||
|
<source>Archives</source>
|
||||||
|
<target state="translated">Archiwum</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">Filtr</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">Ekstraktor</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">Archiwum puste</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="c41475a25c9f9d9639db9efa56637603a77528b4" datatype="html">
|
||||||
|
<source>Download archive</source>
|
||||||
|
<target state="translated">Pobierz archiwum</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">Brak</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="2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4" datatype="html">
|
||||||
|
<source>Video</source>
|
||||||
|
<target state="translated">Wideo</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
|
||||||
|
<context context-type="linenumber">92</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">133</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Video</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="f0baeb8b69d120073b6d60d34785889b0c3232c8" 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.html</context>
|
||||||
|
<context context-type="linenumber">93</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">134</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Audio</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="4b3972c3e9485218508a95f7a4ce7758e3f09ced" datatype="html">
|
||||||
|
<source>Upload</source>
|
||||||
|
<target state="translated">Wyślij</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">Wideo</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="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">Chcesz usunąć archiwum(a) <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="2525880134753073592" datatype="html">
|
||||||
|
<source>Successfully deleted archive items!</source>
|
||||||
|
<target state="translated">Pomyślnie usunięto elementy archiwum!</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="8953483585652369683" datatype="html">
|
||||||
|
<source>Archive successfully imported!</source>
|
||||||
|
<target state="translated">Archiwum zostało pomyślnie zaimportowane!</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="6876310993601590130" datatype="html">
|
||||||
|
<source>Download completed</source>
|
||||||
|
<target state="translated">Pobieranie zakończone</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="7911845622864460134" datatype="html">
|
||||||
|
<source>Video only</source>
|
||||||
|
<target state="translated">Tylko wideo</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="9176960997786930103" datatype="html">
|
||||||
|
<source>Error for: <x id="PH" equiv-text="task['title']"/></source>
|
||||||
|
<target state="translated">Błąd dla: <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="1d4fa01d25990f60abf21c3a451fa8ba262b7912" datatype="html">
|
||||||
|
<source>Unfavorite</source>
|
||||||
|
<target state="translated">Nieulubione</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="1f2809e6a99d511fdb6eaf041d785fe54d0680cc" datatype="html">
|
||||||
|
<source>File card size</source>
|
||||||
|
<target state="translated">Rozmiar karty pliku</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">42</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">Argument</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="1a9b415816364f554ee411020e65219092655271" datatype="html">
|
||||||
|
<source>Title filter</source>
|
||||||
|
<target state="translated">Filtr tytułu</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">Obsługa regex</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="1698114086921246480" datatype="html">
|
||||||
|
<source>Unsubscribe</source>
|
||||||
|
<target state="translated">Anuluj subskrypcję</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="35cf4cdcedc8ef3f94b6100e0d86836e31dbb908" datatype="html">
|
||||||
|
<source>Force autoplay</source>
|
||||||
|
<target state="translated">Wymuś autoodtwarzanie</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">218</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">Włącz kanał RSS</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">271</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Enable RSS Feed setting</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="0ed98b4c6ec1db6708a963e8a2699478ac97f55c" datatype="html">
|
||||||
|
<source>Add subscription</source>
|
||||||
|
<target state="translated">Dodaj subskrypcję</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="8c6e24eab969d9f63a8a0e9d617aee3b99e28ae6" datatype="html">
|
||||||
|
<source>Play all</source>
|
||||||
|
<target state="translated">Odtwórz wszystko</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="6080b77234e92ad41bb52653b239c4c4f851317d" datatype="html">
|
||||||
|
<source>Error</source>
|
||||||
|
<target state="translated">Błąd</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
|
||||||
|
<context context-type="linenumber">39</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Error</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3640026747176198246" datatype="html">
|
||||||
|
<source>Watch content</source>
|
||||||
|
<target state="translated">Oglądaj zawartość</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">50</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8456659390937171831" datatype="html">
|
||||||
|
<source>Show error</source>
|
||||||
|
<target state="translated">Pokaż błąd</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">56</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="1236604279860679031" datatype="html">
|
||||||
|
<source>Restart</source>
|
||||||
|
<target state="translated">Uruchom ponownie</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">62</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9042260521669277115" datatype="html">
|
||||||
|
<source>Pause</source>
|
||||||
|
<target state="translated">Wstrzymaj</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">68</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7182974689040833178" datatype="html">
|
||||||
|
<source>Resume</source>
|
||||||
|
<target state="translated">Wznów</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">74</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||||
|
<context context-type="linenumber">80</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="019d4bd6a5690f0cfa0ecf346a4e6bf7f0d8debb" datatype="html">
|
||||||
|
<source>Remove</source>
|
||||||
|
<target state="translated">Usuń</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="8443034725057696949" datatype="html">
|
||||||
|
<source>Task finished</source>
|
||||||
|
<target state="translated">Zadanie zakończone</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">Odtwarzaj</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">Ponów próbę pobrania</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="5a105e7bd7e7db6ea211fe950fc9f317379acceb" datatype="html">
|
||||||
|
<source>No notifications available</source>
|
||||||
|
<target state="translated">B</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="1879058637439215882" datatype="html">
|
||||||
|
<source>Download error</source>
|
||||||
|
<target state="translated">Błąd pobierania</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="6437411876967154040" datatype="html">
|
||||||
|
<source>Audio only</source>
|
||||||
|
<target state="translated">Tylko 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="3533826530554274875" datatype="html">
|
||||||
|
<source>Upload Date</source>
|
||||||
|
<target state="translated">Data przesłania</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">Nazwa</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">Rozmiar pliku</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">Ustawienia zadania - <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="cdf5297d8d080a78e8b10debc5c38b7845a3cbe7" datatype="html">
|
||||||
|
<source>Do not ask for confirmation</source>
|
||||||
|
<target state="translated">Nie pytaj o potwierdzenie</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="8571838164752006148" datatype="html">
|
||||||
|
<source>View error</source>
|
||||||
|
<target state="translated">Wyświetl błąd</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="4665451070906079743" datatype="html">
|
||||||
|
<source>Favorited</source>
|
||||||
|
<target state="translated">Ulubione</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="7410432243549869948" datatype="html">
|
||||||
|
<source>Duration</source>
|
||||||
|
<target state="translated">Czas trwania</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">Usuń pliki starsze niż</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="7b4585a9072f3c1292972c14a3d0e14978fbfc9c" datatype="html">
|
||||||
|
<source>Delete old files:</source>
|
||||||
|
<target state="translated">Usuń stare pliki:</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="b78a98bc54259a29cf6250dbaeab5fe11fae91cf" datatype="html">
|
||||||
|
<source>Favorited</source>
|
||||||
|
<target state="translated">Ulubione</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="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
|
||||||
|
<source>User</source>
|
||||||
|
<target state="translated">Użytkownik</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="1091872159779006651" datatype="html">
|
||||||
|
<source>You must input a time!</source>
|
||||||
|
<target state="translated">Musisz wprowadzić czas!</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="338b44701a53ce3ef2f36abfb56f89c3edfa9eab" datatype="html">
|
||||||
|
<source>Over</source>
|
||||||
|
<target state="translated">Nad</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">32,34</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Over</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="e0a11fbea353b1ce1131161774e4a3e10bcb99b1" datatype="html">
|
||||||
|
<source>Large</source>
|
||||||
|
<target state="translated">Duży</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">44,46</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Large</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="d57c023a4cf63b2f12c10328c15b636ff18929aa" datatype="html">
|
||||||
|
<source>Best</source>
|
||||||
|
<target state="translated">Najlepsze</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="af30e51aa8b67e1133a341ec28359be05150e65c" datatype="html">
|
||||||
|
<source>No description available.</source>
|
||||||
|
<target state="translated">Opis nie dostępny.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/player/player.component.html</context>
|
||||||
|
<context context-type="linenumber">25,27</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">No description</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">Uważaj, włączając to w trybie wielu użytkowników! Dane użytkownika mogą zostać ujawnione.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">272</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">Zobacz dokumentację tutaj.</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">274</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">RSS feed documentation</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="06f503e492d6dbcf59e7b9c412ca86913d718689" datatype="html">
|
||||||
|
<source>ntfy topic URL</source>
|
||||||
|
<target state="translated">ntfy topic URL</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">ntfy topic URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html">
|
||||||
|
<source>Use gotify API</source>
|
||||||
|
<target state="translated">Użyj gotify API</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">396</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">Adres serwera Gotify</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">400</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 aplikacji Gotify</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">407</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">Użyj Telegram API</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">413</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">Token bota Telegramu</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">417</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Telegram bot token</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="144e1a21ebe8fa238f88d2ac27515ed711cfc9a0" datatype="html">
|
||||||
|
<source>Telegram chat ID</source>
|
||||||
|
<target state="translated">ID czatu Telegram</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">424</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">Jak uzyskać ID czatu Telegram?</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">Telegram chat ID help</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8bcabdf6b16cad0313a86c7e940c5e3ad7f9f8ab" datatype="html">
|
||||||
|
<source>Notifications</source>
|
||||||
|
<target state="translated">Powiadomienia</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">343</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Notifications settings label</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7cedb649779673568447b994463b2882c4e0436a" datatype="html">
|
||||||
|
<source>Slack Webhook URL</source>
|
||||||
|
<target state="translated">Slack Webhook URL</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">380</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Slack Webhook URL</note>
|
||||||
|
</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">Czy chcesz usunąć <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">159</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user