Compare commits

..

35 Commits

Author SHA1 Message Date
Isaac Abadi
a4ca1abb7c Adds token to GH actions for GetTwitchDownloader 2023-05-07 00:51:21 -04:00
Isaac Abadi
d90434c240 Added python3.8-dev/build-essential to dockerfile 2023-05-07 00:24:47 -04:00
Isaac Abadi
7a8e94ee64 Added PR multiarch 2023-05-07 00:22:08 -04:00
Tzahi12345
e573f34cea Merge pull request #893 from Tzahi12345/readme-update
README update
2023-05-05 00:05:03 -04:00
Tzahi12345
52e32d4f0f Changed tcd to Twitch Downloader 2023-05-04 23:44:12 -04:00
Tzahi12345
adb5f2256e Translations source file update 2023-05-04 22:33:48 -04:00
Tzahi12345
59bf6ff86d Merge pull request #888 from Tzahi12345/dependency-updates
Dependency updates
2023-05-04 22:28:50 -04:00
Tzahi12345
5ce2e2a35d Updated name of arm64 TwitchDownloaderCLI asset 2023-05-04 22:19:00 -04:00
Tzahi12345
68fbde8907 Merge pull request #800 from Bastians-Bits/master
Development Documentation
2023-05-03 19:32:07 -04:00
Tzahi12345
62bccb3349 Updated DEVELOPMENT.md to reflect dev config file 2023-05-03 17:01:17 -04:00
Tzahi12345
90d9ac025a Added missing config item from default.json 2023-05-03 16:38:48 -04:00
Tzahi12345
07903131f9 GetTwitchDownloader.py now supports LinuxArm-x64 (not implemeted yet), waiting for this: https://github.com/lay295/TwitchDownloader/pull/703 2023-05-03 14:20:54 -04:00
Tzahi12345
ec3bb3e738 Updated dockerfile to move TwitchDownloaderCLI to /usr/local/bin
Fixed issue that prevented TwitchDownloader from working in windows
2023-05-03 14:04:09 -04:00
Tzahi12345
18fcf4eb61 Added missing copy in dockerfile 2023-05-03 02:01:28 -04:00
Tzahi12345
19f35d6af4 Updated content-loader and ngx-file-drop to improve build times 2023-05-03 01:33:51 -04:00
Tzahi12345
3a918b7059 Updated github actions dependencies (2) 2023-05-03 01:08:52 -04:00
Tzahi12345
7e7da6c0bc Updated github actions dependencies 2023-05-03 01:07:14 -04:00
Tzahi12345
f9f7204deb Updated several dependencies 2023-05-03 01:03:49 -04:00
Tzahi12345
8827d9f3de Added some pruning to shrink docker image size 2023-05-02 23:13:47 -04:00
Tzahi12345
42bc255d6c Removed fingerprintjs2 and sessionID param 2023-05-02 23:04:41 -04:00
Tzahi12345
2df3b9cbfd Removed electron (for now) 2023-05-02 23:02:33 -04:00
Tzahi12345
b859d08d86 Added raspberry pi specific documentation to docker-compose 2023-05-02 22:51:52 -04:00
Tzahi12345
e7325b2dc2 Merge pull request #887 from Tzahi12345/gh-actions-fix
GH actions fix
2023-05-02 19:19:06 -04:00
Tzahi12345
21463762ce Removed rimraf install from package.json 2023-05-02 19:10:40 -04:00
Tzahi12345
b06f6a81bb Force rmraf install 2023-05-01 22:34:25 -04:00
Tzahi12345
82c8146032 Updated nodejs version for backend 2023-05-01 19:56:37 -04:00
Tzahi12345
6f13eab550 Force rimraf to use locally installed version https://stackoverflow.com/questions/49092120/sh-1-rimraf-not-found-whenever-i-run-npm-run-build-within-vagrant-installed-o 2023-05-01 19:52:35 -04:00
Tzahi12345
9d2d70b194 Upgraded node version to v16 2023-05-01 19:43:04 -04:00
Tzahi12345
4e04ceae16 Fixed function call in db.js 2023-05-01 19:31:54 -04:00
Tzahi12345
5eec5ac082 Merge pull request #885 from Tzahi12345/slack-notifications
Added support for slack notifications
2023-05-01 17:21:00 -04:00
Tzahi12345
5253ce8793 Merge pull request #886 from Tzahi12345/archive-improvements
Archive improvements
2023-05-01 17:20:50 -04:00
Tzahi12345
33a99d9c8d Added files_api (migrated functions from db_api that are file related)
Archive dialog can now always be opened
2023-04-29 19:41:34 -04:00
Tzahi12345
0e5c78db0d Modified archive logic to align with #366 2023-04-29 19:34:08 -04:00
Tzahi12345
9a08fc6140 Added support for slack notifications 2023-04-29 15:46:58 -04:00
bastiansbits
575f7eed4e Added a new read me (DEVELOPMENT.md) as starting point for new develope
Added a new VSC launch configuration to start the backend in the debugger
Update the build instruction in README.md (Issue #728)
2022-12-07 14:43:43 +01:00
43 changed files with 954 additions and 1457 deletions

View File

@@ -15,9 +15,9 @@ jobs:
- name: checkout code - name: checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: setup node - name: setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v3
with: with:
node-version: '14' node-version: '16'
cache: 'npm' cache: 'npm'
- name: install dependencies - name: install dependencies
run: | run: |
@@ -33,7 +33,7 @@ jobs:
run: echo "::set-output name=date::$(date +'%Y-%m-%d')" run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: create-json - name: create-json
id: create-json id: create-json
uses: jsdaniell/create-json@1.1.2 uses: jsdaniell/create-json@v1.2.2
with: with:
name: "version.json" name: "version.json"
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}' json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
@@ -55,7 +55,7 @@ jobs:
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
- name: upload build artifact - name: upload build artifact
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v3
with: with:
name: youtubedl-material name: youtubedl-material
path: build path: build

View File

@@ -18,10 +18,21 @@ jobs:
run: echo "::set-output name=date::$(date +'%Y-%m-%d')" run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: create-json - name: create-json
id: create-json id: create-json
uses: jsdaniell/create-json@1.1.2 uses: jsdaniell/create-json@v1.2.2
with: with:
name: "version.json" name: "version.json"
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}' json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/' dir: 'backend/'
- name: Build docker images - name: setup platform emulator
run: docker build . -t tzahi12345/youtubedl-material:nightly-pr uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1
- name: build & push images
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8
#platforms: linux/amd64
push: false
tags: tzahi12345/youtubedl-material:nightly-pr

View File

@@ -27,7 +27,7 @@ jobs:
- name: create-json - name: create-json
id: create-json id: create-json
uses: jsdaniell/create-json@1.1.2 uses: jsdaniell/create-json@v1.2.2
with: with:
name: "version.json" name: "version.json"
json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}' json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
@@ -60,10 +60,10 @@ jobs:
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build - name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -84,3 +84,5 @@ jobs:
push: true push: true
tags: ${{ steps.docker-meta.outputs.tags }} tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }} labels: ${{ steps.docker-meta.outputs.labels }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -34,7 +34,7 @@ jobs:
- name: create-json - name: create-json
id: create-json id: create-json
uses: jsdaniell/create-json@1.1.2 uses: jsdaniell/create-json@v1.2.2
with: with:
name: "version.json" name: "version.json"
json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}' json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
@@ -44,7 +44,7 @@ jobs:
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build - name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Generate Docker image metadata - name: Generate Docker image metadata
id: docker-meta id: docker-meta
@@ -63,7 +63,7 @@ jobs:
type=sha,prefix=sha-,format=short type=sha,prefix=sha-,format=short
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -84,3 +84,6 @@ jobs:
push: true push: true
tags: ${{ steps.docker-meta.outputs.tags }} tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }} labels: ${{ steps.docker-meta.outputs.labels }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

14
.vscode/launch.json vendored
View File

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

38
DEVELOPMENT.md Normal file
View File

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

View File

@@ -37,6 +37,8 @@ COPY [ "src/", "/build/src/" ]
RUN npm install && \ RUN npm install && \
npm run build && \ npm run build && \
ls -al /build/backend/public ls -al /build/backend/public
RUN npm uninstall -g @angular/cli
RUN rm -rf node_modules
# Install backend deps # Install backend deps
@@ -51,7 +53,7 @@ FROM base as python
WORKDIR /app WORKDIR /app
COPY docker-utils/GetTwitchDownloader.py . COPY docker-utils/GetTwitchDownloader.py .
RUN apt update && \ RUN apt update && \
apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip && \ apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip python3.8-dev build-essential && \
apt clean && \ apt clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN pip install PyGithub requests RUN pip install PyGithub requests
@@ -71,6 +73,7 @@ COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ] COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"] COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ] COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
COPY --chown=$UID:$GID --from=python ["/app/TwitchDownloaderCLI","/usr/local/bin/TwitchDownloaderCLI"]
RUN chown $UID:$GID . RUN chown $UID:$GID .
RUN chmod +x /app/fix-scripts/*.sh RUN chmod +x /app/fix-scripts/*.sh
# Add some persistence data # Add some persistence data

View File

@@ -28,13 +28,28 @@ Dark mode:
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide. NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
Debian/Ubuntu: Required dependencies:
* Node.js 16
* Python
Optional dependencies:
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
* [Twitch Downloader CLI](https://github.com/lay295/TwitchDownloader) (for downloading Twitch VOD chats)
<details>
<summary>Debian/Ubuntu</summary>
```bash ```bash
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
``` ```
CentOS 7: </details>
<details>
<summary>CentOS 7</summary>
```bash ```bash
sudo yum install epel-release sudo yum install epel-release
@@ -42,13 +57,11 @@ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfu
sudo yum install centos-release-scl-rh sudo yum install centos-release-scl-rh
sudo yum install rh-nodejs12 sudo yum install rh-nodejs12
scl enable rh-nodejs12 bash scl enable rh-nodejs12 bash
curl -fsSL https://rpm.nodesource.com/setup_16.x | sudo bash -
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
``` ```
Optional dependencies: </details>
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
* [tcd](https://github.com/PetterKraabol/Twitch-Chat-Downloader) (for downloading Twitch VOD chats)
### Installing ### Installing
@@ -72,7 +85,9 @@ If you'd like to install YoutubeDL-Material, go to the Installation section. If
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend. To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `npm build`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder. Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `npm run build`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
Lastly, type `npm -g install pm2` to install pm2 globally.
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`. The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.

View File

@@ -2,7 +2,6 @@ const { uuid } = require('uuidv4');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { promisify } = require('util'); const { promisify } = require('util');
const auth_api = require('./authentication/auth'); const auth_api = require('./authentication/auth');
const winston = require('winston');
const path = require('path'); const path = require('path');
const compression = require('compression'); const compression = require('compression');
const multer = require('multer'); const multer = require('multer');
@@ -19,6 +18,7 @@ const CONSTS = require('./consts')
const read_last_lines = require('read-last-lines'); const read_last_lines = require('read-last-lines');
const ps = require('ps-node'); const ps = require('ps-node');
const Feed = require('feed').Feed; const Feed = require('feed').Feed;
const session = require('express-session');
// needed if bin/details somehow gets deleted // needed if bin/details somehow gets deleted
if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2000.06.06","path":"node_modules\\youtube-dl\\bin\\youtube-dl.exe","exec":"youtube-dl.exe","downloader":"youtube-dl"}) if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2000.06.06","path":"node_modules\\youtube-dl\\bin\\youtube-dl.exe","exec":"youtube-dl.exe","downloader":"youtube-dl"})
@@ -34,6 +34,7 @@ const categories_api = require('./categories');
const twitch_api = require('./twitch'); const twitch_api = require('./twitch');
const youtubedl_api = require('./youtube-dl'); const youtubedl_api = require('./youtube-dl');
const archive_api = require('./archive'); const archive_api = require('./archive');
const files_api = require('./files');
var app = express(); var app = express();
@@ -162,6 +163,7 @@ app.use(bodyParser.json());
// use passport // use passport
app.use(auth_api.passport.initialize()); app.use(auth_api.passport.initialize());
app.use(session({ secret: uuid(), resave: true, saveUninitialized: true }))
app.use(auth_api.passport.session()); app.use(auth_api.passport.session());
// actual functions // actual functions
@@ -173,10 +175,10 @@ async function checkMigrations() {
if (!simplified_db_migration_complete) { if (!simplified_db_migration_complete) {
logger.info('Beginning migration: 4.1->4.2+') logger.info('Beginning migration: 4.1->4.2+')
let success = await simplifyDBFileStructure(); let success = await simplifyDBFileStructure();
success = success && await db_api.addMetadataPropertyToDB('view_count'); success = success && await files_api.addMetadataPropertyToDB('view_count');
success = success && await db_api.addMetadataPropertyToDB('description'); success = success && await files_api.addMetadataPropertyToDB('description');
success = success && await db_api.addMetadataPropertyToDB('height'); success = success && await files_api.addMetadataPropertyToDB('height');
success = success && await db_api.addMetadataPropertyToDB('abr'); success = success && await files_api.addMetadataPropertyToDB('abr');
// sets migration to complete // sets migration to complete
db.set('simplified_db_migration_complete', true).write(); db.set('simplified_db_migration_complete', true).write();
if (success) { logger.info('4.1->4.2+ migration complete!'); } if (success) { logger.info('4.1->4.2+ migration complete!'); }
@@ -724,7 +726,7 @@ const optionalJwt = async function (req, res, next) {
const uuid = using_body ? req.body.uuid : req.query.uuid; const uuid = using_body ? req.body.uuid : req.query.uuid;
const uid = using_body ? req.body.uid : req.query.uid; const uid = using_body ? req.body.uid : req.query.uid;
const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id; const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id;
const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : await db_api.getPlaylist(playlist_id, uuid, true); const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : await files_api.getPlaylist(playlist_id, uuid, true);
if (file) { if (file) {
req.can_watch = true; req.can_watch = true;
return next(); return next();
@@ -935,7 +937,7 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
const sub_id = req.body.sub_id; const sub_id = req.body.sub_id;
const uuid = req.isAuthenticated() ? req.user.uid : null; const uuid = req.isAuthenticated() ? req.user.uid : null;
const {files, file_count} = await db_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid); const {files, file_count} = await files_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
res.send({ res.send({
files: files, files: files,
@@ -1101,7 +1103,7 @@ app.post('/api/incrementViewCount', async (req, res) => {
uuid = req.user.uid; uuid = req.user.uid;
} }
const file_obj = await db_api.getVideo(file_uid, uuid, sub_id); const file_obj = await files_api.getVideo(file_uid, uuid, sub_id);
const current_view_count = file_obj && file_obj['local_view_count'] ? file_obj['local_view_count'] : 0; const current_view_count = file_obj && file_obj['local_view_count'] ? file_obj['local_view_count'] : 0;
const new_view_count = current_view_count + 1; const new_view_count = current_view_count + 1;
@@ -1229,7 +1231,7 @@ app.post('/api/deleteSubscriptionFile', optionalJwt, async (req, res) => {
let deleteForever = req.body.deleteForever; let deleteForever = req.body.deleteForever;
let file_uid = req.body.file_uid; let file_uid = req.body.file_uid;
let success = await db_api.deleteFile(file_uid, deleteForever); let success = await files_api.deleteFile(file_uid, deleteForever);
if (success) { if (success) {
res.send({ res.send({
@@ -1317,7 +1319,7 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
let playlistName = req.body.playlistName; let playlistName = req.body.playlistName;
let uids = req.body.uids; let uids = req.body.uids;
const new_playlist = await db_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null); const new_playlist = await files_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null);
res.send({ res.send({
new_playlist: new_playlist, new_playlist: new_playlist,
@@ -1330,13 +1332,13 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
let uuid = req.body.uuid ? req.body.uuid : (req.user && req.user.uid ? req.user.uid : null); let uuid = req.body.uuid ? req.body.uuid : (req.user && req.user.uid ? req.user.uid : null);
let include_file_metadata = req.body.include_file_metadata; let include_file_metadata = req.body.include_file_metadata;
const playlist = await db_api.getPlaylist(playlist_id, uuid); const playlist = await files_api.getPlaylist(playlist_id, uuid);
const file_objs = []; const file_objs = [];
if (playlist && include_file_metadata) { if (playlist && include_file_metadata) {
for (let i = 0; i < playlist['uids'].length; i++) { for (let i = 0; i < playlist['uids'].length; i++) {
const uid = playlist['uids'][i]; const uid = playlist['uids'][i];
const file_obj = await db_api.getVideo(uid, uuid); const file_obj = await files_api.getVideo(uid, uuid);
if (file_obj) file_objs.push(file_obj); if (file_obj) file_objs.push(file_obj);
// TODO: remove file from playlist if could not be found // TODO: remove file from playlist if could not be found
} }
@@ -1374,7 +1376,7 @@ app.post('/api/addFileToPlaylist', optionalJwt, async (req, res) => {
playlist.uids.push(file_uid); playlist.uids.push(file_uid);
let success = await db_api.updatePlaylist(playlist); let success = await files_api.updatePlaylist(playlist);
res.send({ res.send({
success: success success: success
}); });
@@ -1382,7 +1384,7 @@ app.post('/api/addFileToPlaylist', optionalJwt, async (req, res) => {
app.post('/api/updatePlaylist', optionalJwt, async (req, res) => { app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
let playlist = req.body.playlist; let playlist = req.body.playlist;
let success = await db_api.updatePlaylist(playlist, req.user && req.user.uid); let success = await files_api.updatePlaylist(playlist, req.user && req.user.uid);
res.send({ res.send({
success: success success: success
}); });
@@ -1412,7 +1414,7 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => {
const blacklistMode = req.body.blacklistMode; const blacklistMode = req.body.blacklistMode;
let wasDeleted = false; let wasDeleted = false;
wasDeleted = await db_api.deleteFile(uid, blacklistMode); wasDeleted = await files_api.deleteFile(uid, blacklistMode);
res.send(wasDeleted); res.send(wasDeleted);
}); });
@@ -1444,7 +1446,7 @@ app.post('/api/deleteAllFiles', optionalJwt, async (req, res) => {
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
let wasDeleted = false; let wasDeleted = false;
wasDeleted = await db_api.deleteFile(files[i].uid, blacklistMode); wasDeleted = await files_api.deleteFile(files[i].uid, blacklistMode);
if (wasDeleted) { if (wasDeleted) {
delete_count++; delete_count++;
} }
@@ -1470,10 +1472,10 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
if (playlist_id) { if (playlist_id) {
zip_file_generated = true; zip_file_generated = true;
const playlist_files_to_download = []; const playlist_files_to_download = [];
const playlist = await db_api.getPlaylist(playlist_id, uuid); const playlist = await files_api.getPlaylist(playlist_id, uuid);
for (let i = 0; i < playlist['uids'].length; i++) { for (let i = 0; i < playlist['uids'].length; i++) {
const playlist_file_uid = playlist['uids'][i]; const playlist_file_uid = playlist['uids'][i];
const file_obj = await db_api.getVideo(playlist_file_uid, uuid); const file_obj = await files_api.getVideo(playlist_file_uid, uuid);
playlist_files_to_download.push(file_obj); playlist_files_to_download.push(file_obj);
} }
@@ -1487,7 +1489,7 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
// generate zip // generate zip
file_path_to_download = await utils.createContainerZipFile(sub['name'], sub_files_to_download); file_path_to_download = await utils.createContainerZipFile(sub['name'], sub_files_to_download);
} else { } else {
const file_obj = await db_api.getVideo(uid, uuid, sub_id) const file_obj = await files_api.getVideo(uid, uuid, sub_id)
file_path_to_download = file_obj.path; file_path_to_download = file_obj.path;
} }
if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download); if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download);
@@ -1634,7 +1636,7 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
if (!multiUserMode || req.isAuthenticated() || req.can_watch) { if (!multiUserMode || req.isAuthenticated() || req.can_watch) {
file_obj = await db_api.getVideo(uid, uuid, sub_id); file_obj = await files_api.getVideo(uid, uuid, sub_id);
if (file_obj) file_path = file_obj['path']; if (file_obj) file_path = file_obj['path'];
else file_path = null; else file_path = null;
} }
@@ -2082,7 +2084,7 @@ app.get('/api/rss', async function (req, res) {
const sub_id = req.query.sub_id ? decodeURIComponent(req.query.sub_id) : null; const sub_id = req.query.sub_id ? decodeURIComponent(req.query.sub_id) : null;
const uuid = req.query.uuid ? decodeURIComponent(req.query.uuid) : null; const uuid = req.query.uuid ? decodeURIComponent(req.query.uuid) : null;
const {files} = await db_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid); const {files} = await files_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
const feed = new Feed({ const feed = new Feed({
title: 'Downloads', title: 'Downloads',

View File

@@ -50,7 +50,8 @@
"telegram_bot_token": "", "telegram_bot_token": "",
"telegram_chat_id": "", "telegram_chat_id": "",
"webhook_URL": "", "webhook_URL": "",
"discord_webhook_URL": "" "discord_webhook_URL": "",
"slack_webhook_URL": ""
}, },
"Themes": { "Themes": {
"default_theme": "default", "default_theme": "default",

View File

@@ -220,7 +220,8 @@ const DEFAULT_CONFIG = {
"telegram_bot_token": "", "telegram_bot_token": "",
"telegram_chat_id": "", "telegram_chat_id": "",
"webhook_URL": "", "webhook_URL": "",
"discord_webhook_URL": "" "discord_webhook_URL": "",
"slack_webhook_URL": "",
}, },
"Themes": { "Themes": {
"default_theme": "default", "default_theme": "default",

View File

@@ -162,6 +162,10 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_discord_webhook_url', 'key': 'ytdl_discord_webhook_url',
'path': 'YoutubeDLMaterial.API.discord_webhook_URL' 'path': 'YoutubeDLMaterial.API.discord_webhook_URL'
}, },
'ytdl_slack_webhook_url': {
'key': 'ytdl_slack_webhook_url',
'path': 'YoutubeDLMaterial.API.slack_webhook_URL'
},
// Themes // Themes

View File

@@ -1,11 +1,11 @@
var fs = require('fs-extra') const fs = require('fs-extra')
var path = require('path') const path = require('path')
const { MongoClient } = require("mongodb"); const { MongoClient } = require("mongodb");
const { uuid } = require('uuidv4'); const { uuid } = require('uuidv4');
const _ = require('lodash'); const _ = require('lodash');
const config_api = require('./config'); const config_api = require('./config');
var utils = require('./utils') const utils = require('./utils')
const logger = require('./logger'); const logger = require('./logger');
const low = require('lowdb') const low = require('lowdb')
@@ -167,82 +167,9 @@ exports._connectToDB = async (custom_connection_string = null) => {
} }
} }
exports.registerFileDB = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => { exports.setVideoProperty = async (file_uid, assignment_obj) => {
if (!file_object) file_object = generateFileObject(file_path, type); // TODO: check if video exists, throw error if not
if (!file_object) { await exports.updateRecord('files', {uid: file_uid}, assignment_obj);
logger.error(`Could not find associated JSON file for ${type} file ${file_path}`);
return false;
}
utils.fixVideoMetadataPerms(file_path, type);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_path);
// if category exists, only include essential info
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
// modify duration
if (cropFileSettings) {
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
}
if (user_uid) file_object['user_uid'] = user_uid;
if (sub_id) file_object['sub_id'] = sub_id;
const file_obj = await registerFileDBManual(file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_path, type)
}
return file_obj;
}
async function registerFileDBManual(file_object) {
// add additional info
file_object['uid'] = uuid();
file_object['registered'] = Date.now();
path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
await exports.insertRecordIntoTable('files', file_object, {path: file_object['path']})
return file_object;
}
function generateFileObject(file_path, type) {
var jsonobj = utils.getJSON(file_path, type);
if (!jsonobj) {
return null;
} else if (!jsonobj['_filename']) {
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
return null;
}
const ext = (type === 'audio') ? '.mp3' : '.mp4'
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
// console.
var stats = fs.statSync(true_file_path);
const file_id = utils.removeFileExtension(path.basename(file_path));
var title = jsonobj.title;
var url = jsonobj.webpage_url;
var uploader = jsonobj.uploader;
var upload_date = utils.formatDateString(jsonobj.upload_date);
var size = stats.size;
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var isaudio = type === 'audio';
var description = jsonobj.description;
var file_obj = new utils.File(file_id, title, thumbnail, isaudio, duration, url, uploader, size, true_file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
return file_obj;
}
function getAppendedBasePathSub(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
} }
exports.getFileDirectoriesAndDBs = async () => { exports.getFileDirectoriesAndDBs = async () => {
@@ -317,277 +244,6 @@ exports.getFileDirectoriesAndDBs = async () => {
return dirs_to_check; return dirs_to_check;
} }
exports.importUnregisteredFiles = async () => {
const imported_files = [];
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
// run through check list and check each file to see if it's missing from the db
for (let i = 0; i < dirs_to_check.length; i++) {
const dir_to_check = dirs_to_check[i];
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
for (let j = 0; j < files.length; j++) {
const file = files[j];
// check if file exists in db, if not add it
const files_with_same_url = await exports.getRecords('files', {url: file.url, sub_id: dir_to_check.sub_id});
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
if (!file_is_registered) {
// add additional info
const file_obj = await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
if (file_obj) {
imported_files.push(file_obj['uid']);
logger.verbose(`Added discovered file to the database: ${file.id}`);
} else {
logger.error(`Failed to import ${file['path']} automatically.`);
}
}
}
}
return imported_files;
}
exports.addMetadataPropertyToDB = async (property_key) => {
try {
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
const update_obj = {};
for (let i = 0; i < dirs_to_check.length; i++) {
const dir_to_check = dirs_to_check[i];
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
for (let j = 0; j < files.length; j++) {
const file = files[j];
if (file[property_key]) {
update_obj[file.uid] = {[property_key]: file[property_key]};
}
}
}
return await exports.bulkUpdateRecordsByKey('files', 'uid', update_obj);
} catch(err) {
logger.error(err);
return false;
}
}
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
const first_video = await exports.getVideo(uids[0]);
const thumbnailToUse = first_video['thumbnailURL'];
let new_playlist = {
name: playlist_name,
uids: uids,
id: uuid(),
thumbnailURL: thumbnailToUse,
registered: Date.now(),
randomize_order: false
};
new_playlist.user_uid = user_uid ? user_uid : undefined;
await exports.insertRecordIntoTable('playlists', new_playlist);
const duration = await exports.calculatePlaylistDuration(new_playlist);
await exports.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
return new_playlist;
}
exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => {
let playlist = await exports.getRecord('playlists', {id: playlist_id});
if (!playlist) {
playlist = await exports.getRecord('categories', {uid: playlist_id});
if (playlist) {
const uids = (await exports.getRecords('files', {'category.uid': playlist_id})).map(file => file.uid);
playlist['uids'] = uids;
playlist['auto'] = true;
}
}
// converts playlists to new UID-based schema
if (playlist && playlist['fileNames'] && !playlist['uids']) {
playlist['uids'] = [];
logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`);
for (let i = 0; i < playlist['fileNames'].length; i++) {
const fileName = playlist['fileNames'][i];
const uid = await exports.getVideoUIDByID(fileName, user_uid);
if (uid) playlist['uids'].push(uid);
else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`);
}
exports.updatePlaylist(playlist, user_uid);
}
// prevent unauthorized users from accessing the file info
if (require_sharing && !playlist['sharingEnabled']) return null;
return playlist;
}
exports.updatePlaylist = async (playlist) => {
let playlistID = playlist.id;
const duration = await exports.calculatePlaylistDuration(playlist);
playlist.duration = duration;
return await exports.updateRecord('playlists', {id: playlistID}, playlist);
}
exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = null) => {
let success = await exports.updateRecord('playlists', {id: playlist_id}, assignment_obj);
if (!success) {
success = await exports.updateRecord('categories', {uid: playlist_id}, assignment_obj);
}
if (!success) {
logger.error(`Could not find playlist or category with ID ${playlist_id}`);
}
return success;
}
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
if (!playlist_file_objs) {
playlist_file_objs = [];
for (let i = 0; i < playlist['uids'].length; i++) {
const uid = playlist['uids'][i];
const file_obj = await exports.getVideo(uid);
if (file_obj) playlist_file_objs.push(file_obj);
}
}
return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
}
exports.deleteFile = async (uid, blacklistMode = false) => {
const file_obj = await exports.getVideo(uid);
const type = file_obj.isAudio ? 'audio' : 'video';
const folderPath = path.dirname(file_obj.path);
const ext = type === 'audio' ? 'mp3' : 'mp4';
const name = file_obj.id;
const filePathNoExtension = utils.removeFileExtension(file_obj.path);
var jsonPath = `${file_obj.path}.info.json`;
var altJSONPath = `${filePathNoExtension}.info.json`;
var thumbnailPath = `${filePathNoExtension}.webp`;
var altThumbnailPath = `${filePathNoExtension}.jpg`;
jsonPath = path.join(__dirname, jsonPath);
altJSONPath = path.join(__dirname, altJSONPath);
let jsonExists = await fs.pathExists(jsonPath);
let thumbnailExists = await fs.pathExists(thumbnailPath);
if (!jsonExists) {
if (await fs.pathExists(altJSONPath)) {
jsonExists = true;
jsonPath = altJSONPath;
}
}
if (!thumbnailExists) {
if (await fs.pathExists(altThumbnailPath)) {
thumbnailExists = true;
thumbnailPath = altThumbnailPath;
}
}
let fileExists = await fs.pathExists(file_obj.path);
if (config_api.descriptors[uid]) {
try {
for (let i = 0; i < config_api.descriptors[uid].length; i++) {
config_api.descriptors[uid][i].destroy();
}
} catch(e) {
}
}
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) {
// get id/extractor from JSON
const info_json = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath));
let retrievedID = null;
let retrievedExtractor = null;
if (info_json) {
retrievedID = info_json['id'];
retrievedExtractor = info_json['extractor'];
}
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
if (!blacklistMode) {
// workaround until a files_api is created (using archive_api would make a circular dependency)
await exports.removeAllRecords('archives', {extractor: retrievedExtractor, id: retrievedID, type: type, user_uid: file_obj.user_uid, sub_id: file_obj.sub_id});
// await archive_api.removeFromArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id);
}
}
if (jsonExists) await fs.unlink(jsonPath);
if (thumbnailExists) await fs.unlink(thumbnailPath);
await exports.removeRecord('files', {uid: uid});
if (fileExists) {
await fs.unlink(file_obj.path);
if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) {
return false;
} else {
return true;
}
} else {
// TODO: tell user that the file didn't exist
return true;
}
}
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
exports.getVideoUIDByID = async (file_id, uuid = null) => {
const file_obj = await exports.getRecord('files', {id: file_id});
return file_obj ? file_obj['uid'] : null;
}
exports.getVideo = async (file_uid) => {
return await exports.getRecord('files', {uid: file_uid});
}
exports.getAllFiles = async (sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid) => {
const filter_obj = {user_uid: uuid};
const regex = true;
if (text_search) {
if (regex) {
filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
} else {
filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
}
}
if (favorite_filter) {
filter_obj['favorite'] = true;
}
if (sub_id) {
filter_obj['sub_id'] = sub_id;
}
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
const files = JSON.parse(JSON.stringify(await exports.getRecords('files', filter_obj, false, sort, range, text_search)));
const file_count = await exports.getRecords('files', filter_obj, true);
return {files, file_count};
}
exports.setVideoProperty = async (file_uid, assignment_obj) => {
// TODO: check if video exists, throw error if not
await exports.updateRecord('files', {uid: file_uid}, assignment_obj);
}
// Basic DB functions // Basic DB functions
// Create // Create

View File

@@ -13,6 +13,7 @@ const { create } = require('xmlbuilder2');
const categories_api = require('./categories'); const categories_api = require('./categories');
const utils = require('./utils'); const utils = require('./utils');
const db_api = require('./db'); const db_api = require('./db');
const files_api = require('./files');
const notifications_api = require('./notifications'); const notifications_api = require('./notifications');
const archive_api = require('./archive'); const archive_api = require('./archive');
@@ -221,6 +222,7 @@ async function collectInfo(download_uid) {
return; return;
} }
// in subscriptions we don't care if archive mode is enabled, but we already removed archived videos from subs by this point
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive && !options.ignoreArchive) { if (useYoutubeDLArchive && !options.ignoreArchive) {
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info['id'], type, download['user_uid'], download['sub_id']); const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info['id'], type, download['user_uid'], download['sub_id']);
@@ -384,10 +386,9 @@ async function downloadQueuedFile(download_uid) {
} }
// registers file in DB // registers file in DB
const file_obj = await db_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings); const file_obj = await files_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
if (useYoutubeDLArchive && !options.ignoreArchive) await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
notifications_api.sendDownloadNotification(file_obj, download['user_uid']); notifications_api.sendDownloadNotification(file_obj, download['user_uid']);
@@ -399,7 +400,7 @@ async function downloadQueuedFile(download_uid) {
if (file_objs.length > 1) { if (file_objs.length > 1) {
// create playlist // create playlist
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', '); const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']); container = await files_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
} else if (file_objs.length === 1) { } else if (file_objs.length === 1) {
container = file_objs[0]; container = file_objs[0];
} else { } else {

350
backend/files.js Normal file
View File

@@ -0,0 +1,350 @@
const fs = require('fs-extra')
const path = require('path')
const { uuid } = require('uuidv4');
const config_api = require('./config');
const db_api = require('./db');
const archive_api = require('./archive');
const utils = require('./utils')
const logger = require('./logger');
exports.registerFileDB = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
if (!file_object) file_object = generateFileObject(file_path, type);
if (!file_object) {
logger.error(`Could not find associated JSON file for ${type} file ${file_path}`);
return false;
}
utils.fixVideoMetadataPerms(file_path, type);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_path);
// if category exists, only include essential info
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
// modify duration
if (cropFileSettings) {
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
}
if (user_uid) file_object['user_uid'] = user_uid;
if (sub_id) file_object['sub_id'] = sub_id;
const file_obj = await registerFileDBManual(file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_path, type)
}
return file_obj;
}
async function registerFileDBManual(file_object) {
// add additional info
file_object['uid'] = uuid();
file_object['registered'] = Date.now();
const path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
await db_api.insertRecordIntoTable('files', file_object, {path: file_object['path']})
return file_object;
}
function generateFileObject(file_path, type) {
const jsonobj = utils.getJSON(file_path, type);
if (!jsonobj) {
return null;
} else if (!jsonobj['_filename']) {
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
return null;
}
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
// console.
const stats = fs.statSync(true_file_path);
const file_id = utils.removeFileExtension(path.basename(file_path));
const title = jsonobj.title;
const url = jsonobj.webpage_url;
const uploader = jsonobj.uploader;
const upload_date = utils.formatDateString(jsonobj.upload_date);
const size = stats.size;
const thumbnail = jsonobj.thumbnail;
const duration = jsonobj.duration;
const isaudio = type === 'audio';
const description = jsonobj.description;
const file_obj = new utils.File(file_id, title, thumbnail, isaudio, duration, url, uploader, size, true_file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
return file_obj;
}
exports.importUnregisteredFiles = async () => {
const imported_files = [];
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
// run through check list and check each file to see if it's missing from the db
for (let i = 0; i < dirs_to_check.length; i++) {
const dir_to_check = dirs_to_check[i];
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
for (let j = 0; j < files.length; j++) {
const file = files[j];
// check if file exists in db, if not add it
const files_with_same_url = await db_api.getRecords('files', {url: file.url, sub_id: dir_to_check.sub_id});
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
if (!file_is_registered) {
// add additional info
const file_obj = await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
if (file_obj) {
imported_files.push(file_obj['uid']);
logger.verbose(`Added discovered file to the database: ${file.id}`);
} else {
logger.error(`Failed to import ${file['path']} automatically.`);
}
}
}
}
return imported_files;
}
exports.addMetadataPropertyToDB = async (property_key) => {
try {
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
const update_obj = {};
for (let i = 0; i < dirs_to_check.length; i++) {
const dir_to_check = dirs_to_check[i];
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
for (let j = 0; j < files.length; j++) {
const file = files[j];
if (file[property_key]) {
update_obj[file.uid] = {[property_key]: file[property_key]};
}
}
}
return await db_api.bulkUpdateRecordsByKey('files', 'uid', update_obj);
} catch(err) {
logger.error(err);
return false;
}
}
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
const first_video = await exports.getVideo(uids[0]);
const thumbnailToUse = first_video['thumbnailURL'];
let new_playlist = {
name: playlist_name,
uids: uids,
id: uuid(),
thumbnailURL: thumbnailToUse,
registered: Date.now(),
randomize_order: false
};
new_playlist.user_uid = user_uid ? user_uid : undefined;
await db_api.insertRecordIntoTable('playlists', new_playlist);
const duration = await exports.calculatePlaylistDuration(new_playlist);
await db_api.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
return new_playlist;
}
exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => {
let playlist = await db_api.getRecord('playlists', {id: playlist_id});
if (!playlist) {
playlist = await db_api.getRecord('categories', {uid: playlist_id});
if (playlist) {
const uids = (await db_api.getRecords('files', {'category.uid': playlist_id})).map(file => file.uid);
playlist['uids'] = uids;
playlist['auto'] = true;
}
}
// converts playlists to new UID-based schema
if (playlist && playlist['fileNames'] && !playlist['uids']) {
playlist['uids'] = [];
logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`);
for (let i = 0; i < playlist['fileNames'].length; i++) {
const fileName = playlist['fileNames'][i];
const uid = await exports.getVideoUIDByID(fileName, user_uid);
if (uid) playlist['uids'].push(uid);
else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`);
}
exports.updatePlaylist(playlist, user_uid);
}
// prevent unauthorized users from accessing the file info
if (require_sharing && !playlist['sharingEnabled']) return null;
return playlist;
}
exports.updatePlaylist = async (playlist) => {
let playlistID = playlist.id;
const duration = await exports.calculatePlaylistDuration(playlist);
playlist.duration = duration;
return await db_api.updateRecord('playlists', {id: playlistID}, playlist);
}
exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = null) => {
let success = await db_api.updateRecord('playlists', {id: playlist_id}, assignment_obj);
if (!success) {
success = await db_api.updateRecord('categories', {uid: playlist_id}, assignment_obj);
}
if (!success) {
logger.error(`Could not find playlist or category with ID ${playlist_id}`);
}
return success;
}
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
if (!playlist_file_objs) {
playlist_file_objs = [];
for (let i = 0; i < playlist['uids'].length; i++) {
const uid = playlist['uids'][i];
const file_obj = await exports.getVideo(uid);
if (file_obj) playlist_file_objs.push(file_obj);
}
}
return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
}
exports.deleteFile = async (uid, blacklistMode = false) => {
const file_obj = await exports.getVideo(uid);
const type = file_obj.isAudio ? 'audio' : 'video';
const folderPath = path.dirname(file_obj.path);
const name = file_obj.id;
const filePathNoExtension = utils.removeFileExtension(file_obj.path);
var jsonPath = `${file_obj.path}.info.json`;
var altJSONPath = `${filePathNoExtension}.info.json`;
var thumbnailPath = `${filePathNoExtension}.webp`;
var altThumbnailPath = `${filePathNoExtension}.jpg`;
jsonPath = path.join(__dirname, jsonPath);
altJSONPath = path.join(__dirname, altJSONPath);
let jsonExists = await fs.pathExists(jsonPath);
let thumbnailExists = await fs.pathExists(thumbnailPath);
if (!jsonExists) {
if (await fs.pathExists(altJSONPath)) {
jsonExists = true;
jsonPath = altJSONPath;
}
}
if (!thumbnailExists) {
if (await fs.pathExists(altThumbnailPath)) {
thumbnailExists = true;
thumbnailPath = altThumbnailPath;
}
}
let fileExists = await fs.pathExists(file_obj.path);
if (config_api.descriptors[uid]) {
try {
for (let i = 0; i < config_api.descriptors[uid].length; i++) {
config_api.descriptors[uid][i].destroy();
}
} catch(e) {
}
}
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive || file_obj.sub_id) {
// get id/extractor from JSON
const info_json = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath));
let retrievedID = null;
let retrievedExtractor = null;
if (info_json) {
retrievedID = info_json['id'];
retrievedExtractor = info_json['extractor'];
}
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
if (!blacklistMode) {
await archive_api.removeFromArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id)
} else {
const exists_in_archive = await archive_api.existsInArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id);
if (!exists_in_archive) {
await archive_api.addToArchive(retrievedExtractor, retrievedID, type, file_obj.title, file_obj.user_uid, file_obj.sub_id);
}
}
}
if (jsonExists) await fs.unlink(jsonPath);
if (thumbnailExists) await fs.unlink(thumbnailPath);
await db_api.removeRecord('files', {uid: uid});
if (fileExists) {
await fs.unlink(file_obj.path);
if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) {
return false;
} else {
return true;
}
} else {
// TODO: tell user that the file didn't exist
return true;
}
}
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
exports.getVideoUIDByID = async (file_id, uuid = null) => {
const file_obj = await db_api.getRecord('files', {id: file_id});
return file_obj ? file_obj['uid'] : null;
}
exports.getVideo = async (file_uid) => {
return await db_api.getRecord('files', {uid: file_uid});
}
exports.getAllFiles = async (sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid) => {
const filter_obj = {user_uid: uuid};
const regex = true;
if (text_search) {
if (regex) {
filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
} else {
filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
}
}
if (favorite_filter) {
filter_obj['favorite'] = true;
}
if (sub_id) {
filter_obj['sub_id'] = sub_id;
}
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
const files = JSON.parse(JSON.stringify(await db_api.getRecords('files', filter_obj, false, sort, range, text_search)));
const file_count = await db_api.getRecords('files', filter_obj, true);
return {files, file_count};
}

View File

@@ -64,6 +64,9 @@ exports.sendNotification = async (notification) => {
if (config_api.getConfigItem('ytdl_discord_webhook_url')) { if (config_api.getConfigItem('ytdl_discord_webhook_url')) {
sendDiscordNotification(data); sendDiscordNotification(data);
} }
if (config_api.getConfigItem('ytdl_slack_webhook_url')) {
sendSlackNotification(data);
}
await db_api.insertRecordIntoTable('notifications', notification); await db_api.insertRecordIntoTable('notifications', notification);
return notification; return notification;
@@ -174,6 +177,65 @@ async function sendDiscordNotification({body, title, type, url, thumbnail}) {
return result; return result;
} }
function sendSlackNotification({body, title, type, url, thumbnail}) {
const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url');
logger.verbose(`Sending slack notification to ${slack_webhook_url}`);
const data = {
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `*${title}*`
}
},
{
type: "section",
text: {
type: "plain_text",
text: body
}
}
]
}
// add thumbnail if exists
if (thumbnail) {
data['blocks'].push({
type: "image",
image_url: thumbnail,
alt_text: "notification_thumbnail"
});
}
data['blocks'].push(
{
type: "section",
text: {
type: "mrkdwn",
text: `<${url}|${url}>`
}
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `*ID:* ${type}`
}
]
}
);
fetch(slack_webhook_url, {
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data),
});
}
function sendGenericNotification(data) { function sendGenericNotification(data) {
const webhook_url = config_api.getConfigItem('ytdl_webhook_url'); const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
logger.verbose(`Sending generic notification to ${webhook_url}`); logger.verbose(`Sending generic notification to ${webhook_url}`);

View File

@@ -2036,26 +2036,20 @@
} }
}, },
"jsonwebtoken": { "jsonwebtoken": {
"version": "8.5.1", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
"requires": { "requires": {
"jws": "^3.2.2", "jws": "^3.2.2",
"lodash.includes": "^4.3.0", "lodash": "^4.17.21",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1", "ms": "^2.1.1",
"semver": "^5.6.0" "semver": "^7.3.8"
}, },
"dependencies": { "dependencies": {
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
} }
} }
}, },
@@ -2200,41 +2194,11 @@
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
}, },
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
},
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
},
"lodash.isplainobject": { "lodash.isplainobject": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
}, },
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"lodash.union": { "lodash.union": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
@@ -2633,11 +2597,21 @@
} }
}, },
"node-id3": { "node-id3": {
"version": "0.1.16", "version": "0.2.6",
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.1.16.tgz", "resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.6.tgz",
"integrity": "sha512-neWBJZxwrWnnebqy0b6gOGpnOPu1l1ASlusVCJUlrgr55ksftcz3lPbP/h4KaFXN+WQX7hh+kmNwkj5DMAa7KA==", "integrity": "sha512-w8GuKXLlPpDjTxLowCt/uYMhRQzED3cg2GdSG1i6RSGKeDzPvxlXeLQuQInKljahPZ0aDnmyX7FX8BbJOM7REg==",
"requires": { "requires": {
"iconv-lite": "^0.4.15" "iconv-lite": "0.6.2"
},
"dependencies": {
"iconv-lite": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
} }
}, },
"node-schedule": { "node-schedule": {
@@ -2862,12 +2836,13 @@
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
}, },
"passport": { "passport": {
"version": "0.4.1", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
"integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
"requires": { "requires": {
"passport-strategy": "1.x.x", "passport-strategy": "1.x.x",
"pause": "0.0.1" "pause": "0.0.1",
"utils-merge": "^1.0.1"
} }
}, },
"passport-http": { "passport-http": {
@@ -3288,9 +3263,12 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
}, },
"semver": { "semver": {
"version": "5.7.1", "version": "7.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==",
"requires": {
"lru-cache": "^6.0.0"
}
}, },
"send": { "send": {
"version": "0.18.0", "version": "0.18.0",

View File

@@ -17,6 +17,10 @@
"bugs": { "bugs": {
"url": "" "url": ""
}, },
"engines": {
"node": "^16",
"npm": "6.14.4"
},
"homepage": "", "homepage": "",
"dependencies": { "dependencies": {
"@discordjs/builders": "^1.6.1", "@discordjs/builders": "^1.6.1",
@@ -34,7 +38,7 @@
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0", "fs-extra": "^9.0.0",
"gotify": "^1.1.0", "gotify": "^1.1.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lowdb": "^1.0.0", "lowdb": "^1.0.0",
"md5": "^2.2.1", "md5": "^2.2.1",
@@ -43,10 +47,10 @@
"mongodb": "^3.6.9", "mongodb": "^3.6.9",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"node-id3": "^0.1.14", "node-id3": "^0.2.6",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"node-telegram-bot-api": "^0.61.0", "node-telegram-bot-api": "^0.61.0",
"passport": "^0.4.1", "passport": "^0.6.0",
"passport-http": "^0.3.0", "passport-http": "^0.3.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-ldapauth": "^3.0.1", "passport-ldapauth": "^3.0.1",

View File

@@ -199,8 +199,13 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
return false; return false;
} else { } else {
// check if the user wants the video to be redownloaded (deleteForever === false) // check if the user wants the video to be redownloaded (deleteForever === false)
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); if (deleteForever) {
if (useArchive && !deleteForever) { // ensure video is in the archives
const exists_in_archive = await archive_api.existsInArchive(retrievedExtractor, retrievedID, sub.type, user_uid, sub.id);
if (!exists_in_archive) {
await archive_api.addToArchive(retrievedExtractor, retrievedID, sub.type, file.title, user_uid, sub.id);
}
} else {
await archive_api.removeFromArchive(retrievedExtractor, retrievedID, sub.type, user_uid, sub.id); await archive_api.removeFromArchive(retrievedExtractor, retrievedID, sub.type, user_uid, sub.id);
} }
return true; return true;
@@ -364,15 +369,12 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
downloadConfig.push(...qualityPath) downloadConfig.push(...qualityPath)
// if archive is being used, we want to quickly skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time) // skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
if (useYoutubeDLArchive) { logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id); const archive_path = path.join(appendedBasePath, 'archive.txt');
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`) await fs.writeFile(archive_path, archive_text);
const archive_path = path.join(appendedBasePath, 'archive.txt'); downloadConfig.push('--download-archive', archive_path);
await fs.writeFile(archive_path, archive_text);
downloadConfig.push('--download-archive', archive_path);
}
if (sub.custom_args) { if (sub.custom_args) {
const customArgsArray = sub.custom_args.split(',,'); const customArgsArray = sub.custom_args.split(',,');
@@ -428,11 +430,8 @@ async function getFilesToDownload(sub, output_jsons) {
logger.info(`Skipping adding file ${output_json['_filename']} for subscription ${sub.name} as a file with that path already exists.`) logger.info(`Skipping adding file ${output_json['_filename']} for subscription ${sub.name} as a file with that path already exists.`)
continue; continue;
} }
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); const exists_in_archive = await archive_api.existsInArchive(output_json['extractor'], output_json['id'], sub.type, sub.user_uid, sub.id);
if (useYoutubeDLArchive) { if (exists_in_archive) continue;
const exists_in_archive = await archive_api.existsInArchive(output_json['extractor'], output_json['id'], sub.type, sub.user_uid, sub.id);
if (exists_in_archive) continue;
}
files_to_download.push(output_json); files_to_download.push(output_json);
} }

View File

@@ -2,6 +2,7 @@ const db_api = require('./db');
const notifications_api = require('./notifications'); const notifications_api = require('./notifications');
const youtubedl_api = require('./youtube-dl'); const youtubedl_api = require('./youtube-dl');
const archive_api = require('./archive'); const archive_api = require('./archive');
const files_api = require('./files');
const fs = require('fs-extra'); const fs = require('fs-extra');
const logger = require('./logger'); const logger = require('./logger');
@@ -20,7 +21,7 @@ const TASKS = {
job: null job: null
}, },
missing_db_records: { missing_db_records: {
run: db_api.importUnregisteredFiles, run: files_api.importUnregisteredFiles,
title: 'Import missing DB records', title: 'Import missing DB records',
job: null job: null
}, },
@@ -259,7 +260,7 @@ async function autoDeleteFiles(data) {
logger.info(`Removing ${data['files_to_remove'].length} old files!`); logger.info(`Removing ${data['files_to_remove'].length} old files!`);
for (let i = 0; i < data['files_to_remove'].length; i++) { for (let i = 0; i < data['files_to_remove'].length; i++) {
const file_to_remove = data['files_to_remove'][i]; const file_to_remove = data['files_to_remove'][i];
await db_api.deleteFile(file_to_remove['uid'], task_obj['options']['blacklist_files'] || (file_to_remove['sub_id'] && file_to_remove['blacklist_subscription_files'])); await files_api.deleteFile(file_to_remove['uid'], task_obj['options']['blacklist_files'] || (file_to_remove['sub_id'] && file_to_remove['blacklist_subscription_files']));
} }
} }
} }

View File

@@ -40,6 +40,7 @@ const utils = require('../utils');
const subscriptions_api = require('../subscriptions'); const subscriptions_api = require('../subscriptions');
const archive_api = require('../archive'); const archive_api = require('../archive');
const categories_api = require('../categories'); const categories_api = require('../categories');
const files_api = require('../files');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { uuid } = require('uuidv4'); const { uuid } = require('uuidv4');
const NodeID3 = require('node-id3'); const NodeID3 = require('node-id3');
@@ -356,7 +357,7 @@ describe('Multi User', async function() {
}); });
const video_to_test = sample_video_json['uid']; const video_to_test = sample_video_json['uid'];
it('Get video', async function() { it('Get video', async function() {
const video_obj = await db_api.getVideo(video_to_test); const video_obj = await files_api.getVideo(video_to_test);
assert(video_obj); assert(video_obj);
}); });
@@ -374,12 +375,12 @@ describe('Multi User', async function() {
}); });
describe('Zip generators', function() { describe('Zip generators', function() {
it('Playlist zip generator', async function() { it('Playlist zip generator', async function() {
const playlist = await db_api.getPlaylist(playlist_to_test, user_to_test); const playlist = await files_api.getPlaylist(playlist_to_test, user_to_test);
assert(playlist); assert(playlist);
const playlist_files_to_download = []; const playlist_files_to_download = [];
for (let i = 0; i < playlist['uids'].length; i++) { for (let i = 0; i < playlist['uids'].length; i++) {
const uid = playlist['uids'][i]; const uid = playlist['uids'][i];
const playlist_file = await db_api.getVideo(uid, user_to_test); const playlist_file = await files_api.getVideo(uid, user_to_test);
playlist_files_to_download.push(playlist_file); playlist_files_to_download.push(playlist_file);
} }
const zip_path = await utils.createContainerZipFile(playlist, playlist_files_to_download); const zip_path = await utils.createContainerZipFile(playlist, playlist_files_to_download);
@@ -407,7 +408,7 @@ describe('Multi User', async function() {
// const sub_to_test = ''; // const sub_to_test = '';
// const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e'; // const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e';
// it('Get video', async function() { // it('Get video', async function() {
// const video_obj = db_api.getVideo(video_to_test, 'admin', ); // const video_obj = files_api.getVideo(video_to_test, 'admin', );
// assert(video_obj); // assert(video_obj);
// }); // });

View File

@@ -25,7 +25,7 @@ async function getCommentsForVOD(vodId) {
return null; return null;
} }
const result = await exec(`TwitchDownloaderCLI chatdownload -u ${vodId} -o appdata/${vodId}.json`, {stdio:[0,1,2]}); const result = await exec(`${cliPath} chatdownload -u ${vodId} -o appdata/${vodId}.json`, {stdio:[0,1,2]});
if (result['stderr']) { if (result['stderr']) {
logger.error(`Failed to download twitch comments for ${vodId}`); logger.error(`Failed to download twitch comments for ${vodId}`);

View File

@@ -1,7 +0,0 @@
Copyright 2018 Isaac Grynsztein
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,3 +0,0 @@
The official YoutubeDL-Material chart
Repo: https://github.com/Tzahi12345/YoutubeDL-Material

View File

@@ -1,4 +0,0 @@
# Artifact Hub repository metadata file
repositoryID: ff79ae03-57c1-4f18-8368-5e085d06e2f1
owners:
- name: tzahi12345

View File

@@ -1,22 +0,0 @@
apiVersion: v1
entries:
youtubedl-material:
- apiVersion: v1
created: "2021-01-01T05:51:36.304331-05:00"
description: A Material Design frontend for youtube-dl
digest: f7340d24fb051ade30b890db3b5c483a884b8459316dcf2ffc6fb9413d41252e
home: https://github.com/Tzahi12345/YoutubeDL-Material/
icon: https://i.imgur.com/IKOlr0N.png
keywords:
- youtubedl-material
- youtube-dl
maintainers:
- email: IsaacMGrynsztein@gmail.com
name: tzahi12345
name: youtubedl-material
sources:
- https://github.com/Tzahi12345/YoutubeDL-Material/
urls:
- youtubedl-material-0.0.1.tgz
version: 0.0.1
generated: "2021-01-01T05:51:36.3003569-05:00"

View File

@@ -1,14 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
creationTimestamp: null
labels:
io.kompose.service: ytdl-material-claim-appdata
name: ytdl-material-claim-appdata
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ default "1Gi" .Values.appdataClaimSize }}
status: {}

View File

@@ -1,14 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
creationTimestamp: null
labels:
io.kompose.service: ytdl-material-claim-audio
name: ytdl-material-claim-audio
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ default "50Gi" .Values.audioClaimSize }}
status: {}

View File

@@ -1,14 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
creationTimestamp: null
labels:
io.kompose.service: ytdl-material-claim-subscriptions
name: ytdl-material-claim-subscriptions
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ default "50Gi" .Values.subscriptionsClaimSize }}
status: {}

View File

@@ -1,14 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
creationTimestamp: null
labels:
io.kompose.service: ytdl-material-claim-users
name: ytdl-material-claim-users
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ default "50Gi" .Values.usersClaimSize }}
status: {}

View File

@@ -1,14 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
creationTimestamp: null
labels:
io.kompose.service: ytdl-material-claim-video
name: ytdl-material-claim-video
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ default "50Gi" .Values.videoClaimSize }}
status: {}

Binary file not shown.

View File

@@ -18,6 +18,7 @@ services:
- "8998:17442" - "8998:17442"
image: tzahi12345/youtubedl-material:latest image: tzahi12345/youtubedl-material:latest
ytdl-mongo-db: ytdl-mongo-db:
# If you are using a Raspberry Pi, use mongo:4.4.18
image: mongo:4 image: mongo:4
logging: logging:
driver: "none" driver: "none"

View File

@@ -3,17 +3,31 @@ import requests
import shutil import shutil
import os import os
import re import re
import sys
from collections import OrderedDict
from github import Github from github import Github
machine = platform.machine() machine = platform.machine()
def isARM(): # https://stackoverflow.com/questions/45125516/possible-values-for-uname-m
return True if machine.startswith('arm') else False MACHINES_TO_ZIP = OrderedDict([
("x86_64", "Linux-x64"),
("aarch64", "LinuxArm64"),
("armv8", "LinuxArm64"),
("arm", "LinuxArm"),
("AMD64", "Windows-x64")
])
def getZipName():
for possibleMachine, possibleZipName in MACHINES_TO_ZIP.items():
if possibleMachine in machine:
return possibleZipName
def getLatestFileInRepo(repo, search_string): def getLatestFileInRepo(repo, search_string):
# Create an unauthenticated instance of the Github object # Create an unauthenticated instance of the Github object
g = Github(os.environ.get('GH_TOKEN')) gh_token = os.environ.get('GH_TOKEN')
g = Github(gh_token if gh_token else None) # ensure it's none if it's falsy
# Replace with the repository owner and name # Replace with the repository owner and name
repo = g.get_repo(repo) repo = g.get_repo(repo)
@@ -46,8 +60,11 @@ def getLatestFileInRepo(repo, search_string):
print(f'No release found with {search_string}') print(f'No release found with {search_string}')
def getLatestCLIRelease(): def getLatestCLIRelease():
isArm = isARM() zipName = getZipName()
searchString = r'.*CLI.*' + "LinuxArm.zip" if isArm else "Linux-x64.zip" if not zipName:
print(f"GetTwitchDownloader.py could not get valid path for '{machine}'. Exiting...")
sys.exit(1)
searchString = r'.*CLI.*' + zipName
getLatestFileInRepo("lay295/TwitchDownloader", searchString) getLatestFileInRepo("lay295/TwitchDownloader", searchString)
getLatestCLIRelease() getLatestCLIRelease()

821
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,18 +34,17 @@
"@angular/platform-browser-dynamic": "^15.0.1", "@angular/platform-browser-dynamic": "^15.0.1",
"@angular/router": "^15.0.1", "@angular/router": "^15.0.1",
"@fontsource/material-icons": "^4.5.4", "@fontsource/material-icons": "^4.5.4",
"@ngneat/content-loader": "^5.0.0", "@ngneat/content-loader": "^7.0.0",
"@videogular/ngx-videogular": "^6.0.0", "@videogular/ngx-videogular": "^6.0.0",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"filesize": "^10.0.7", "filesize": "^10.0.7",
"fingerprintjs2": "^2.1.0",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"material-icons": "^1.10.8", "material-icons": "^1.10.8",
"nan": "^2.14.1", "nan": "^2.14.1",
"ngx-avatars": "^1.4.1", "ngx-avatars": "^1.4.1",
"ngx-file-drop": "^13.0.0", "ngx-file-drop": "^15.0.0",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"rxjs-compat": "^6.6.7", "rxjs-compat": "^6.6.7",
"tslib": "^2.0.0", "tslib": "^2.0.0",
@@ -66,7 +65,6 @@
"@typescript-eslint/parser": "^4.29.0", "@typescript-eslint/parser": "^4.29.0",
"ajv": "^7.2.4", "ajv": "^7.2.4",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"electron": "^19.1.9",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"jasmine-core": "~3.6.0", "jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",

View File

@@ -21,7 +21,7 @@
<mat-icon>person</mat-icon> <mat-icon>person</mat-icon>
<span i18n="Profile menu label">Profile</span> <span i18n="Profile menu label">Profile</span>
</button> </button>
<button *ngIf="postsService.config && postsService.config.Downloader.use_youtubedl_archive" class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item> <button class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
<mat-icon>topic</mat-icon> <mat-icon>topic</mat-icon>
<span i18n="Archives menu label">Archives</span> <span i18n="Archives menu label">Archives</span>
</button> </button>

View File

@@ -88,7 +88,7 @@
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0"> <ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<mat-selection-list *ngIf="!normal_files_received"> <mat-selection-list *ngIf="!normal_files_received">
<mat-list-option *ngFor="let file of paged_data"> <mat-list-option *ngFor="let file of paged_data">
<content-loader class="list-ghosts" [primaryColor]="postsService.theme.ghost_primary" [secondaryColor]="postsService.theme.ghost_secondary" [width]="250" [height]="8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader> <content-loader class="list-ghosts" [backgroundColor]="postsService.theme.ghost_primary" [foregroundColor]="postsService.theme.ghost_secondary" viewBox="0 0 250 8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
</mat-list-option> </mat-list-option>
</mat-selection-list> </mat-selection-list>
</ng-container> </ng-container>

View File

@@ -5,7 +5,7 @@
<ng-container i18n="Auto-generated label" *ngIf="file_obj.auto">Auto-generated</ng-container> <ng-container i18n="Auto-generated label" *ngIf="file_obj.auto">Auto-generated</ng-container>
<ng-container *ngIf="!file_obj.auto">{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</ng-container> <ng-container *ngIf="!file_obj.auto">{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</ng-container>
</div> </div>
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" [width]="250" [height]="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div> <div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 250 30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
<!-- The context menu trigger must be kept above the "more info" menu --> <!-- The context menu trigger must be kept above the "more info" menu -->
<div style="visibility: hidden; position: fixed" <div style="visibility: hidden; position: fixed"
[style.left]="contextMenuPosition.x" [style.left]="contextMenuPosition.x"
@@ -35,14 +35,9 @@
</ng-container> </ng-container>
</mat-menu> </mat-menu>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item> <button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container></button>
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
</button>
<button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item>
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete and don't download again</ng-container>
</button>
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button> <button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and don't download again">Delete and don't download again</ng-container></button> <button *ngIf="file_obj.sub_id || use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and don't download again">Delete and don't download again</ng-container></button>
</ng-container> </ng-container>
<ng-container *ngIf="is_playlist && !loading"> <ng-container *ngIf="is_playlist && !loading">
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button> <button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
@@ -68,11 +63,11 @@
</div> </div>
<div *ngIf="loading" class="img-div"> <div *ngIf="loading" class="img-div">
<content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" [width]="100" [height]="55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader> <content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 100 55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader>
</div> </div>
<span *ngIf="!loading" [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }">{{card_size === 'large' && file_obj.uploader ? file_obj.uploader + ' - ' : ''}}<strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span> <span *ngIf="!loading" [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }">{{card_size === 'large' && file_obj.uploader ? file_obj.uploader + ' - ' : ''}}<strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
<span *ngIf="loading" class="title-loading"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" [width]="250" [height]="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span> <span *ngIf="loading" class="title-loading"><content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 250 30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span>
</div> </div>
</mat-card> </mat-card>
</div> </div>

View File

@@ -50,7 +50,6 @@ export class MainComponent implements OnInit {
allowQualitySelect = false; allowQualitySelect = false;
downloadOnlyMode = false; downloadOnlyMode = false;
forceAutoplay = false; forceAutoplay = false;
use_youtubedl_archive = false;
globalCustomArgs = null; globalCustomArgs = null;
allowAdvancedDownload = false; allowAdvancedDownload = false;
useDefaultDownloadingAgent = true; useDefaultDownloadingAgent = true;
@@ -188,7 +187,6 @@ export class MainComponent implements OnInit {
&& this.postsService.hasPermission('filemanager'); && this.postsService.hasPermission('filemanager');
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode']; this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
this.forceAutoplay = this.postsService.config['Extra']['force_autoplay']; this.forceAutoplay = this.postsService.config['Extra']['force_autoplay'];
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
this.globalCustomArgs = this.postsService.config['Downloader']['custom_args']; this.globalCustomArgs = this.postsService.config['Downloader']['custom_args'];
this.youtubeSearchEnabled = this.postsService.config['API'] && this.postsService.config['API']['use_youtube_API'] && this.youtubeSearchEnabled = this.postsService.config['API'] && this.postsService.config['API']['use_youtube_API'] &&
this.postsService.config['API']['youtube_API_key']; this.postsService.config['API']['youtube_API_key'];

View File

@@ -8,7 +8,6 @@ import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import * as Fingerprint2 from 'fingerprintjs2';
import { import {
ChangeRolePermissionsRequest, ChangeRolePermissionsRequest,
ChangeUserPermissionsRequest, ChangeUserPermissionsRequest,
@@ -131,7 +130,6 @@ export class PostsService implements CanActivate {
// auth // auth
auth_token = '4241b401-7236-493e-92b5-b72696b9d853'; auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
session_id = null;
httpOptions: { httpOptions: {
params: HttpParams params: HttpParams
}; };
@@ -187,12 +185,6 @@ export class PostsService implements CanActivate {
}) })
}; };
Fingerprint2.get(components => {
// set identity as user id doesn't necessarily exist
this.session_id = Fingerprint2.x64hash128(components.map(function (pair) { return pair.value; }).join(), 31);
this.httpOptions.params = this.httpOptions.params.set('sessionID', this.session_id);
});
const redirect_not_required = window.location.href.includes('/player') || window.location.href.includes('/login'); const redirect_not_required = window.location.href.includes('/player') || window.location.href.includes('/login');
// get config // get config
@@ -796,7 +788,7 @@ export class PostsService implements CanActivate {
resetHttpParams() { resetHttpParams() {
// resets http params // resets http params
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}` this.http_params = `apiKey=${this.auth_token}`
this.httpOptions = { this.httpOptions = {
params: new HttpParams({ params: new HttpParams({

View File

@@ -387,9 +387,16 @@
</div> </div>
<div class="col-12 mb-2 mt-3"> <div class="col-12 mb-2 mt-3">
<mat-form-field class="text-field" color="accent"> <mat-form-field class="text-field" color="accent">
<mat-label i18n="Discord webhook URL">Discord Webhook URL</mat-label> <mat-label i18n="Discord Webhook URL">Discord Webhook URL</mat-label>
<input placeholder="https://discord.com/api/webhooks/<webhook_id>/<webhook_token>" [(ngModel)]="new_config['API']['discord_webhook_URL']" matInput> <input placeholder="https://discord.com/api/webhooks/<webhook_id>/<webhook_token>" [(ngModel)]="new_config['API']['discord_webhook_URL']" matInput>
<mat-hint><a target="_blank" href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"><ng-container i18n="Gotify API setting hint">See docs here.</ng-container></a></mat-hint> <mat-hint><a target="_blank" href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"><ng-container i18n="Discord API setting hint">See docs here.</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mb-2 mt-3">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="Slack Webhook URL">Slack Webhook URL</mat-label>
<input placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" [(ngModel)]="new_config['API']['slack_webhook_URL']" matInput>
<mat-hint><a target="_blank" href="https://api.slack.com/messaging/webhooks"><ng-container i18n="Slack API setting hint">See docs here.</ng-container></a></mat-hint>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12 mt-3"> <div class="col-12 mt-3">

View File

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