mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adb5f2256e | ||
|
|
59bf6ff86d | ||
|
|
5ce2e2a35d | ||
|
|
68fbde8907 | ||
|
|
62bccb3349 | ||
|
|
90d9ac025a | ||
|
|
07903131f9 | ||
|
|
ec3bb3e738 | ||
|
|
18fcf4eb61 | ||
|
|
19f35d6af4 | ||
|
|
3a918b7059 | ||
|
|
7e7da6c0bc | ||
|
|
f9f7204deb | ||
|
|
8827d9f3de | ||
|
|
42bc255d6c | ||
|
|
2df3b9cbfd | ||
|
|
b859d08d86 | ||
|
|
e7325b2dc2 | ||
|
|
21463762ce | ||
|
|
b06f6a81bb | ||
|
|
82c8146032 | ||
|
|
6f13eab550 | ||
|
|
9d2d70b194 | ||
|
|
4e04ceae16 | ||
|
|
5eec5ac082 | ||
|
|
5253ce8793 | ||
|
|
33a99d9c8d | ||
|
|
0e5c78db0d | ||
|
|
9a08fc6140 | ||
|
|
575f7eed4e |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -15,9 +15,9 @@ jobs:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
- name: install dependencies
|
||||
run: |
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
uses: jsdaniell/create-json@v1.2.2
|
||||
with:
|
||||
name: "version.json"
|
||||
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/*.json -Destination ./build/youtubedl-material
|
||||
- name: upload build artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: youtubedl-material
|
||||
path: build
|
||||
|
||||
2
.github/workflows/docker-pr.yml
vendored
2
.github/workflows/docker-pr.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
uses: jsdaniell/create-json@v1.2.2
|
||||
with:
|
||||
name: "version.json"
|
||||
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||
|
||||
6
.github/workflows/docker-release.yml
vendored
6
.github/workflows/docker-release.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
uses: jsdaniell/create-json@v1.2.2
|
||||
with:
|
||||
name: "version.json"
|
||||
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
|
||||
|
||||
- name: setup multi-arch docker build
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
uses: jsdaniell/create-json@v1.2.2
|
||||
with:
|
||||
name: "version.json"
|
||||
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
|
||||
|
||||
- name: setup multi-arch docker build
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Docker image metadata
|
||||
id: docker-meta
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
type=sha,prefix=sha-,format=short
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@@ -4,6 +4,20 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Dev: Debug Backend",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run-script",
|
||||
"debug"
|
||||
],
|
||||
"runtimeExecutable": "npm",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
|
||||
38
DEVELOPMENT.md
Normal file
38
DEVELOPMENT.md
Normal file
@@ -0,0 +1,38 @@
|
||||
<h1>Development</h1>
|
||||
|
||||
- [First time...](#first-time)
|
||||
- [Setup](#setup)
|
||||
- [Startup](#startup)
|
||||
- [Debugging the backend (VSC)](#debugging-the-backend-vsc)
|
||||
- [Deploy changes](#deploy-changes)
|
||||
- [Frontend](#frontend)
|
||||
- [Backend](#backend)
|
||||
|
||||
# First time...
|
||||
|
||||
## Setup
|
||||
Checkout the repository and navigate to the `youtubedl-material` directory.
|
||||
```bash
|
||||
vim ./src/assets/default.json # Edit settings for your local environment. This config file is just the dev config file, if YTDL_MODE is not set to "debug", then ./backend/appdata/default.json will be used
|
||||
npm -g install pm2 # Install pm2
|
||||
npm install # Install dependencies for the frontend
|
||||
cd ./backend
|
||||
npm install # Install dependencies for the backend
|
||||
cd ..
|
||||
npm run build # Build the frontend
|
||||
```
|
||||
This step have to be done only once.
|
||||
|
||||
## Startup
|
||||
Navigate to the `youtubedl-material/backend` directory and run `npm start`.
|
||||
|
||||
# Debugging the backend (VSC)
|
||||
Open the `youtubedl-material` directory in Visual Studio Code and run the launch configuration `Dev: Debug Backend`.
|
||||
|
||||
# Deploy changes
|
||||
|
||||
## Frontend
|
||||
Navigate to the `youtubedl-material` directory and run `npm run build`. Restart the backend.
|
||||
|
||||
## Backend
|
||||
Simply restart the backend.
|
||||
@@ -37,6 +37,8 @@ COPY [ "src/", "/build/src/" ]
|
||||
RUN npm install && \
|
||||
npm run build && \
|
||||
ls -al /build/backend/public
|
||||
RUN npm uninstall -g @angular/cli
|
||||
RUN rm -rf node_modules
|
||||
|
||||
|
||||
# Install backend deps
|
||||
@@ -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=backend ["/app/","/app/"]
|
||||
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 chmod +x /app/fix-scripts/*.sh
|
||||
# Add some persistence data
|
||||
|
||||
30
README.md
30
README.md
@@ -28,13 +28,29 @@ Dark mode:
|
||||
|
||||
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`)
|
||||
* [tcd](https://github.com/PetterKraabol/Twitch-Chat-Downloader) (for downloading Twitch VOD chats)
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Debian/Ubuntu</summary>
|
||||
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
|
||||
```
|
||||
|
||||
CentOS 7:
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>CentOS 7</summary>
|
||||
|
||||
```bash
|
||||
sudo yum install epel-release
|
||||
@@ -42,13 +58,11 @@ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfu
|
||||
sudo yum install centos-release-scl-rh
|
||||
sudo yum install rh-nodejs12
|
||||
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
|
||||
```
|
||||
|
||||
Optional dependencies:
|
||||
|
||||
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
||||
* [tcd](https://github.com/PetterKraabol/Twitch-Chat-Downloader) (for downloading Twitch VOD chats)
|
||||
</details>
|
||||
|
||||
### Installing
|
||||
|
||||
@@ -72,7 +86,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.
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ const { uuid } = require('uuidv4');
|
||||
const fs = require('fs-extra');
|
||||
const { promisify } = require('util');
|
||||
const auth_api = require('./authentication/auth');
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
const compression = require('compression');
|
||||
const multer = require('multer');
|
||||
@@ -19,6 +18,7 @@ const CONSTS = require('./consts')
|
||||
const read_last_lines = require('read-last-lines');
|
||||
const ps = require('ps-node');
|
||||
const Feed = require('feed').Feed;
|
||||
const session = require('express-session');
|
||||
|
||||
// needed if bin/details somehow gets deleted
|
||||
if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2000.06.06","path":"node_modules\\youtube-dl\\bin\\youtube-dl.exe","exec":"youtube-dl.exe","downloader":"youtube-dl"})
|
||||
@@ -34,6 +34,7 @@ const categories_api = require('./categories');
|
||||
const twitch_api = require('./twitch');
|
||||
const youtubedl_api = require('./youtube-dl');
|
||||
const archive_api = require('./archive');
|
||||
const files_api = require('./files');
|
||||
|
||||
var app = express();
|
||||
|
||||
@@ -162,6 +163,7 @@ app.use(bodyParser.json());
|
||||
|
||||
// use passport
|
||||
app.use(auth_api.passport.initialize());
|
||||
app.use(session({ secret: uuid(), resave: true, saveUninitialized: true }))
|
||||
app.use(auth_api.passport.session());
|
||||
|
||||
// actual functions
|
||||
@@ -173,10 +175,10 @@ async function checkMigrations() {
|
||||
if (!simplified_db_migration_complete) {
|
||||
logger.info('Beginning migration: 4.1->4.2+')
|
||||
let success = await simplifyDBFileStructure();
|
||||
success = success && await db_api.addMetadataPropertyToDB('view_count');
|
||||
success = success && await db_api.addMetadataPropertyToDB('description');
|
||||
success = success && await db_api.addMetadataPropertyToDB('height');
|
||||
success = success && await db_api.addMetadataPropertyToDB('abr');
|
||||
success = success && await files_api.addMetadataPropertyToDB('view_count');
|
||||
success = success && await files_api.addMetadataPropertyToDB('description');
|
||||
success = success && await files_api.addMetadataPropertyToDB('height');
|
||||
success = success && await files_api.addMetadataPropertyToDB('abr');
|
||||
// sets migration to complete
|
||||
db.set('simplified_db_migration_complete', true).write();
|
||||
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 uid = using_body ? req.body.uid : req.query.uid;
|
||||
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) {
|
||||
req.can_watch = true;
|
||||
return next();
|
||||
@@ -935,7 +937,7 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
||||
const sub_id = req.body.sub_id;
|
||||
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({
|
||||
files: files,
|
||||
@@ -1101,7 +1103,7 @@ app.post('/api/incrementViewCount', async (req, res) => {
|
||||
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 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 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) {
|
||||
res.send({
|
||||
@@ -1317,7 +1319,7 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
|
||||
let playlistName = req.body.playlistName;
|
||||
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({
|
||||
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 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 = [];
|
||||
|
||||
if (playlist && include_file_metadata) {
|
||||
for (let i = 0; i < playlist['uids'].length; 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);
|
||||
// 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);
|
||||
|
||||
let success = await db_api.updatePlaylist(playlist);
|
||||
let success = await files_api.updatePlaylist(playlist);
|
||||
res.send({
|
||||
success: success
|
||||
});
|
||||
@@ -1382,7 +1384,7 @@ app.post('/api/addFileToPlaylist', optionalJwt, async (req, res) => {
|
||||
|
||||
app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
|
||||
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({
|
||||
success: success
|
||||
});
|
||||
@@ -1412,7 +1414,7 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => {
|
||||
const blacklistMode = req.body.blacklistMode;
|
||||
|
||||
let wasDeleted = false;
|
||||
wasDeleted = await db_api.deleteFile(uid, blacklistMode);
|
||||
wasDeleted = await files_api.deleteFile(uid, blacklistMode);
|
||||
res.send(wasDeleted);
|
||||
});
|
||||
|
||||
@@ -1444,7 +1446,7 @@ app.post('/api/deleteAllFiles', optionalJwt, async (req, res) => {
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let wasDeleted = false;
|
||||
wasDeleted = await db_api.deleteFile(files[i].uid, blacklistMode);
|
||||
wasDeleted = await files_api.deleteFile(files[i].uid, blacklistMode);
|
||||
if (wasDeleted) {
|
||||
delete_count++;
|
||||
}
|
||||
@@ -1470,10 +1472,10 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
|
||||
if (playlist_id) {
|
||||
zip_file_generated = true;
|
||||
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++) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1487,7 +1489,7 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
|
||||
// generate zip
|
||||
file_path_to_download = await utils.createContainerZipFile(sub['name'], sub_files_to_download);
|
||||
} 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;
|
||||
}
|
||||
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');
|
||||
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'];
|
||||
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 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({
|
||||
title: 'Downloads',
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
"telegram_bot_token": "",
|
||||
"telegram_chat_id": "",
|
||||
"webhook_URL": "",
|
||||
"discord_webhook_URL": ""
|
||||
"discord_webhook_URL": "",
|
||||
"slack_webhook_URL": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
|
||||
@@ -220,7 +220,8 @@ const DEFAULT_CONFIG = {
|
||||
"telegram_bot_token": "",
|
||||
"telegram_chat_id": "",
|
||||
"webhook_URL": "",
|
||||
"discord_webhook_URL": ""
|
||||
"discord_webhook_URL": "",
|
||||
"slack_webhook_URL": "",
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
|
||||
@@ -162,6 +162,10 @@ exports.CONFIG_ITEMS = {
|
||||
'key': 'ytdl_discord_webhook_url',
|
||||
'path': 'YoutubeDLMaterial.API.discord_webhook_URL'
|
||||
},
|
||||
'ytdl_slack_webhook_url': {
|
||||
'key': 'ytdl_slack_webhook_url',
|
||||
'path': 'YoutubeDLMaterial.API.slack_webhook_URL'
|
||||
},
|
||||
|
||||
|
||||
// Themes
|
||||
|
||||
356
backend/db.js
356
backend/db.js
@@ -1,11 +1,11 @@
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const { MongoClient } = require("mongodb");
|
||||
const { uuid } = require('uuidv4');
|
||||
const _ = require('lodash');
|
||||
|
||||
const config_api = require('./config');
|
||||
var utils = require('./utils')
|
||||
const utils = require('./utils')
|
||||
const logger = require('./logger');
|
||||
|
||||
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) => {
|
||||
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();
|
||||
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.setVideoProperty = async (file_uid, assignment_obj) => {
|
||||
// TODO: check if video exists, throw error if not
|
||||
await exports.updateRecord('files', {uid: file_uid}, assignment_obj);
|
||||
}
|
||||
|
||||
exports.getFileDirectoriesAndDBs = async () => {
|
||||
@@ -317,277 +244,6 @@ exports.getFileDirectoriesAndDBs = async () => {
|
||||
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
|
||||
|
||||
// Create
|
||||
|
||||
@@ -13,6 +13,7 @@ const { create } = require('xmlbuilder2');
|
||||
const categories_api = require('./categories');
|
||||
const utils = require('./utils');
|
||||
const db_api = require('./db');
|
||||
const files_api = require('./files');
|
||||
const notifications_api = require('./notifications');
|
||||
const archive_api = require('./archive');
|
||||
|
||||
@@ -221,6 +222,7 @@ async function collectInfo(download_uid) {
|
||||
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');
|
||||
if (useYoutubeDLArchive && !options.ignoreArchive) {
|
||||
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
|
||||
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');
|
||||
if (useYoutubeDLArchive && !options.ignoreArchive) await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
|
||||
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']);
|
||||
|
||||
@@ -399,7 +400,7 @@ async function downloadQueuedFile(download_uid) {
|
||||
if (file_objs.length > 1) {
|
||||
// create playlist
|
||||
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) {
|
||||
container = file_objs[0];
|
||||
} else {
|
||||
|
||||
350
backend/files.js
Normal file
350
backend/files.js
Normal file
@@ -0,0 +1,350 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const { uuid } = require('uuidv4');
|
||||
|
||||
const config_api = require('./config');
|
||||
const db_api = require('./db');
|
||||
const archive_api = require('./archive');
|
||||
const utils = require('./utils')
|
||||
const logger = require('./logger');
|
||||
|
||||
exports.registerFileDB = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
|
||||
if (!file_object) file_object = generateFileObject(file_path, type);
|
||||
if (!file_object) {
|
||||
logger.error(`Could not find associated JSON file for ${type} file ${file_path}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
utils.fixVideoMetadataPerms(file_path, type);
|
||||
|
||||
// add thumbnail path
|
||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_path);
|
||||
|
||||
// if category exists, only include essential info
|
||||
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
|
||||
|
||||
// modify duration
|
||||
if (cropFileSettings) {
|
||||
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
|
||||
}
|
||||
|
||||
if (user_uid) file_object['user_uid'] = user_uid;
|
||||
if (sub_id) file_object['sub_id'] = sub_id;
|
||||
|
||||
const file_obj = await registerFileDBManual(file_object);
|
||||
|
||||
// remove metadata JSON if needed
|
||||
if (!config_api.getConfigItem('ytdl_include_metadata')) {
|
||||
utils.deleteJSONFile(file_path, type)
|
||||
}
|
||||
|
||||
return file_obj;
|
||||
}
|
||||
|
||||
async function registerFileDBManual(file_object) {
|
||||
// add additional info
|
||||
file_object['uid'] = uuid();
|
||||
file_object['registered'] = Date.now();
|
||||
const path_object = path.parse(file_object['path']);
|
||||
file_object['path'] = path.format(path_object);
|
||||
|
||||
await db_api.insertRecordIntoTable('files', file_object, {path: file_object['path']})
|
||||
|
||||
return file_object;
|
||||
}
|
||||
|
||||
function generateFileObject(file_path, type) {
|
||||
const jsonobj = utils.getJSON(file_path, type);
|
||||
if (!jsonobj) {
|
||||
return null;
|
||||
} else if (!jsonobj['_filename']) {
|
||||
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
|
||||
return null;
|
||||
}
|
||||
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
|
||||
// console.
|
||||
const stats = fs.statSync(true_file_path);
|
||||
|
||||
const file_id = utils.removeFileExtension(path.basename(file_path));
|
||||
const title = jsonobj.title;
|
||||
const url = jsonobj.webpage_url;
|
||||
const uploader = jsonobj.uploader;
|
||||
const upload_date = utils.formatDateString(jsonobj.upload_date);
|
||||
|
||||
const size = stats.size;
|
||||
|
||||
const thumbnail = jsonobj.thumbnail;
|
||||
const duration = jsonobj.duration;
|
||||
const isaudio = type === 'audio';
|
||||
const description = jsonobj.description;
|
||||
const file_obj = new utils.File(file_id, title, thumbnail, isaudio, duration, url, uploader, size, true_file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
||||
return file_obj;
|
||||
}
|
||||
|
||||
exports.importUnregisteredFiles = async () => {
|
||||
const imported_files = [];
|
||||
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
|
||||
|
||||
// run through check list and check each file to see if it's missing from the db
|
||||
for (let i = 0; i < dirs_to_check.length; i++) {
|
||||
const dir_to_check = dirs_to_check[i];
|
||||
// recursively get all files in dir's path
|
||||
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
|
||||
|
||||
for (let j = 0; j < files.length; j++) {
|
||||
const file = files[j];
|
||||
|
||||
// check if file exists in db, if not add it
|
||||
const files_with_same_url = await db_api.getRecords('files', {url: file.url, sub_id: dir_to_check.sub_id});
|
||||
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
|
||||
if (!file_is_registered) {
|
||||
// add additional info
|
||||
const file_obj = await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
|
||||
if (file_obj) {
|
||||
imported_files.push(file_obj['uid']);
|
||||
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
||||
} else {
|
||||
logger.error(`Failed to import ${file['path']} automatically.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return imported_files;
|
||||
}
|
||||
|
||||
exports.addMetadataPropertyToDB = async (property_key) => {
|
||||
try {
|
||||
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
|
||||
const update_obj = {};
|
||||
for (let i = 0; i < dirs_to_check.length; i++) {
|
||||
const dir_to_check = dirs_to_check[i];
|
||||
|
||||
// recursively get all files in dir's path
|
||||
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
|
||||
for (let j = 0; j < files.length; j++) {
|
||||
const file = files[j];
|
||||
if (file[property_key]) {
|
||||
update_obj[file.uid] = {[property_key]: file[property_key]};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await db_api.bulkUpdateRecordsByKey('files', 'uid', update_obj);
|
||||
} catch(err) {
|
||||
logger.error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
|
||||
const first_video = await exports.getVideo(uids[0]);
|
||||
const thumbnailToUse = first_video['thumbnailURL'];
|
||||
|
||||
let new_playlist = {
|
||||
name: playlist_name,
|
||||
uids: uids,
|
||||
id: uuid(),
|
||||
thumbnailURL: thumbnailToUse,
|
||||
registered: Date.now(),
|
||||
randomize_order: false
|
||||
};
|
||||
|
||||
new_playlist.user_uid = user_uid ? user_uid : undefined;
|
||||
|
||||
await db_api.insertRecordIntoTable('playlists', new_playlist);
|
||||
|
||||
const duration = await exports.calculatePlaylistDuration(new_playlist);
|
||||
await db_api.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
|
||||
|
||||
return new_playlist;
|
||||
}
|
||||
|
||||
exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => {
|
||||
let playlist = await db_api.getRecord('playlists', {id: playlist_id});
|
||||
|
||||
if (!playlist) {
|
||||
playlist = await db_api.getRecord('categories', {uid: playlist_id});
|
||||
if (playlist) {
|
||||
const uids = (await db_api.getRecords('files', {'category.uid': playlist_id})).map(file => file.uid);
|
||||
playlist['uids'] = uids;
|
||||
playlist['auto'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// converts playlists to new UID-based schema
|
||||
if (playlist && playlist['fileNames'] && !playlist['uids']) {
|
||||
playlist['uids'] = [];
|
||||
logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`);
|
||||
for (let i = 0; i < playlist['fileNames'].length; i++) {
|
||||
const fileName = playlist['fileNames'][i];
|
||||
const uid = await exports.getVideoUIDByID(fileName, user_uid);
|
||||
if (uid) playlist['uids'].push(uid);
|
||||
else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`);
|
||||
}
|
||||
exports.updatePlaylist(playlist, user_uid);
|
||||
}
|
||||
|
||||
// prevent unauthorized users from accessing the file info
|
||||
if (require_sharing && !playlist['sharingEnabled']) return null;
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
exports.updatePlaylist = async (playlist) => {
|
||||
let playlistID = playlist.id;
|
||||
|
||||
const duration = await exports.calculatePlaylistDuration(playlist);
|
||||
playlist.duration = duration;
|
||||
|
||||
return await db_api.updateRecord('playlists', {id: playlistID}, playlist);
|
||||
}
|
||||
|
||||
exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = null) => {
|
||||
let success = await db_api.updateRecord('playlists', {id: playlist_id}, assignment_obj);
|
||||
|
||||
if (!success) {
|
||||
success = await db_api.updateRecord('categories', {uid: playlist_id}, assignment_obj);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
logger.error(`Could not find playlist or category with ID ${playlist_id}`);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
|
||||
if (!playlist_file_objs) {
|
||||
playlist_file_objs = [];
|
||||
for (let i = 0; i < playlist['uids'].length; i++) {
|
||||
const uid = playlist['uids'][i];
|
||||
const file_obj = await exports.getVideo(uid);
|
||||
if (file_obj) playlist_file_objs.push(file_obj);
|
||||
}
|
||||
}
|
||||
|
||||
return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
|
||||
}
|
||||
|
||||
exports.deleteFile = async (uid, blacklistMode = false) => {
|
||||
const file_obj = await exports.getVideo(uid);
|
||||
const type = file_obj.isAudio ? 'audio' : 'video';
|
||||
const folderPath = path.dirname(file_obj.path);
|
||||
const name = file_obj.id;
|
||||
const filePathNoExtension = utils.removeFileExtension(file_obj.path);
|
||||
|
||||
var jsonPath = `${file_obj.path}.info.json`;
|
||||
var altJSONPath = `${filePathNoExtension}.info.json`;
|
||||
var thumbnailPath = `${filePathNoExtension}.webp`;
|
||||
var altThumbnailPath = `${filePathNoExtension}.jpg`;
|
||||
|
||||
jsonPath = path.join(__dirname, jsonPath);
|
||||
altJSONPath = path.join(__dirname, altJSONPath);
|
||||
|
||||
let jsonExists = await fs.pathExists(jsonPath);
|
||||
let thumbnailExists = await fs.pathExists(thumbnailPath);
|
||||
|
||||
if (!jsonExists) {
|
||||
if (await fs.pathExists(altJSONPath)) {
|
||||
jsonExists = true;
|
||||
jsonPath = altJSONPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (!thumbnailExists) {
|
||||
if (await fs.pathExists(altThumbnailPath)) {
|
||||
thumbnailExists = true;
|
||||
thumbnailPath = altThumbnailPath;
|
||||
}
|
||||
}
|
||||
|
||||
let fileExists = await fs.pathExists(file_obj.path);
|
||||
|
||||
if (config_api.descriptors[uid]) {
|
||||
try {
|
||||
for (let i = 0; i < config_api.descriptors[uid].length; i++) {
|
||||
config_api.descriptors[uid][i].destroy();
|
||||
}
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useYoutubeDLArchive || file_obj.sub_id) {
|
||||
// get id/extractor from JSON
|
||||
|
||||
const info_json = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath));
|
||||
let retrievedID = null;
|
||||
let retrievedExtractor = null;
|
||||
if (info_json) {
|
||||
retrievedID = info_json['id'];
|
||||
retrievedExtractor = info_json['extractor'];
|
||||
}
|
||||
|
||||
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
|
||||
if (!blacklistMode) {
|
||||
await archive_api.removeFromArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id)
|
||||
} else {
|
||||
const exists_in_archive = await archive_api.existsInArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id);
|
||||
if (!exists_in_archive) {
|
||||
await archive_api.addToArchive(retrievedExtractor, retrievedID, type, file_obj.title, file_obj.user_uid, file_obj.sub_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonExists) await fs.unlink(jsonPath);
|
||||
if (thumbnailExists) await fs.unlink(thumbnailPath);
|
||||
|
||||
await db_api.removeRecord('files', {uid: uid});
|
||||
|
||||
if (fileExists) {
|
||||
await fs.unlink(file_obj.path);
|
||||
if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// TODO: tell user that the file didn't exist
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
|
||||
exports.getVideoUIDByID = async (file_id, uuid = null) => {
|
||||
const file_obj = await db_api.getRecord('files', {id: file_id});
|
||||
return file_obj ? file_obj['uid'] : null;
|
||||
}
|
||||
|
||||
exports.getVideo = async (file_uid) => {
|
||||
return await db_api.getRecord('files', {uid: file_uid});
|
||||
}
|
||||
|
||||
exports.getAllFiles = async (sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid) => {
|
||||
const filter_obj = {user_uid: uuid};
|
||||
const regex = true;
|
||||
if (text_search) {
|
||||
if (regex) {
|
||||
filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
|
||||
} else {
|
||||
filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
|
||||
}
|
||||
}
|
||||
|
||||
if (favorite_filter) {
|
||||
filter_obj['favorite'] = true;
|
||||
}
|
||||
|
||||
if (sub_id) {
|
||||
filter_obj['sub_id'] = sub_id;
|
||||
}
|
||||
|
||||
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
|
||||
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
|
||||
|
||||
const files = JSON.parse(JSON.stringify(await db_api.getRecords('files', filter_obj, false, sort, range, text_search)));
|
||||
const file_count = await db_api.getRecords('files', filter_obj, true);
|
||||
|
||||
return {files, file_count};
|
||||
}
|
||||
@@ -64,6 +64,9 @@ exports.sendNotification = async (notification) => {
|
||||
if (config_api.getConfigItem('ytdl_discord_webhook_url')) {
|
||||
sendDiscordNotification(data);
|
||||
}
|
||||
if (config_api.getConfigItem('ytdl_slack_webhook_url')) {
|
||||
sendSlackNotification(data);
|
||||
}
|
||||
|
||||
await db_api.insertRecordIntoTable('notifications', notification);
|
||||
return notification;
|
||||
@@ -174,6 +177,65 @@ async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
||||
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) {
|
||||
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
|
||||
logger.verbose(`Sending generic notification to ${webhook_url}`);
|
||||
|
||||
88
backend/package-lock.json
generated
88
backend/package-lock.json
generated
@@ -2036,26 +2036,20 @@
|
||||
}
|
||||
},
|
||||
"jsonwebtoken": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
|
||||
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
|
||||
"integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
|
||||
"requires": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"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",
|
||||
"lodash": "^4.17.21",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^5.6.0"
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2200,41 +2194,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"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": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"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": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
@@ -2633,11 +2597,21 @@
|
||||
}
|
||||
},
|
||||
"node-id3": {
|
||||
"version": "0.1.16",
|
||||
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.1.16.tgz",
|
||||
"integrity": "sha512-neWBJZxwrWnnebqy0b6gOGpnOPu1l1ASlusVCJUlrgr55ksftcz3lPbP/h4KaFXN+WQX7hh+kmNwkj5DMAa7KA==",
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.6.tgz",
|
||||
"integrity": "sha512-w8GuKXLlPpDjTxLowCt/uYMhRQzED3cg2GdSG1i6RSGKeDzPvxlXeLQuQInKljahPZ0aDnmyX7FX8BbJOM7REg==",
|
||||
"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": {
|
||||
@@ -2862,12 +2836,13 @@
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
||||
},
|
||||
"passport": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz",
|
||||
"integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
|
||||
"integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
|
||||
"requires": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1"
|
||||
"pause": "0.0.1",
|
||||
"utils-merge": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"passport-http": {
|
||||
@@ -3288,9 +3263,12 @@
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz",
|
||||
"integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"send": {
|
||||
"version": "0.18.0",
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
"bugs": {
|
||||
"url": ""
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16",
|
||||
"npm": "6.14.4"
|
||||
},
|
||||
"homepage": "",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.6.1",
|
||||
@@ -34,7 +38,7 @@
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"gotify": "^1.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lowdb": "^1.0.0",
|
||||
"md5": "^2.2.1",
|
||||
@@ -43,10 +47,10 @@
|
||||
"mongodb": "^3.6.9",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-id3": "^0.1.14",
|
||||
"node-id3": "^0.2.6",
|
||||
"node-schedule": "^2.1.0",
|
||||
"node-telegram-bot-api": "^0.61.0",
|
||||
"passport": "^0.4.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-ldapauth": "^3.0.1",
|
||||
|
||||
@@ -199,8 +199,13 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
||||
return false;
|
||||
} else {
|
||||
// check if the user wants the video to be redownloaded (deleteForever === false)
|
||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useArchive && !deleteForever) {
|
||||
if (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);
|
||||
}
|
||||
return true;
|
||||
@@ -364,15 +369,12 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
|
||||
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)
|
||||
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useYoutubeDLArchive) {
|
||||
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
||||
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
|
||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||
await fs.writeFile(archive_path, archive_text);
|
||||
downloadConfig.push('--download-archive', archive_path);
|
||||
}
|
||||
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
||||
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
||||
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
|
||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||
await fs.writeFile(archive_path, archive_text);
|
||||
downloadConfig.push('--download-archive', archive_path);
|
||||
|
||||
if (sub.custom_args) {
|
||||
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.`)
|
||||
continue;
|
||||
}
|
||||
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useYoutubeDLArchive) {
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ const db_api = require('./db');
|
||||
const notifications_api = require('./notifications');
|
||||
const youtubedl_api = require('./youtube-dl');
|
||||
const archive_api = require('./archive');
|
||||
const files_api = require('./files');
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const logger = require('./logger');
|
||||
@@ -20,7 +21,7 @@ const TASKS = {
|
||||
job: null
|
||||
},
|
||||
missing_db_records: {
|
||||
run: db_api.importUnregisteredFiles,
|
||||
run: files_api.importUnregisteredFiles,
|
||||
title: 'Import missing DB records',
|
||||
job: null
|
||||
},
|
||||
@@ -259,7 +260,7 @@ async function autoDeleteFiles(data) {
|
||||
logger.info(`Removing ${data['files_to_remove'].length} old files!`);
|
||||
for (let i = 0; i < data['files_to_remove'].length; 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']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ const utils = require('../utils');
|
||||
const subscriptions_api = require('../subscriptions');
|
||||
const archive_api = require('../archive');
|
||||
const categories_api = require('../categories');
|
||||
const files_api = require('../files');
|
||||
const fs = require('fs-extra');
|
||||
const { uuid } = require('uuidv4');
|
||||
const NodeID3 = require('node-id3');
|
||||
@@ -356,7 +357,7 @@ describe('Multi User', async function() {
|
||||
});
|
||||
const video_to_test = sample_video_json['uid'];
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -374,12 +375,12 @@ describe('Multi User', async function() {
|
||||
});
|
||||
describe('Zip generators', 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);
|
||||
const playlist_files_to_download = [];
|
||||
for (let i = 0; i < playlist['uids'].length; 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);
|
||||
}
|
||||
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 video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e';
|
||||
// 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);
|
||||
// });
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ async function getCommentsForVOD(vodId) {
|
||||
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']) {
|
||||
logger.error(`Failed to download twitch comments for ${vodId}`);
|
||||
|
||||
@@ -18,6 +18,7 @@ services:
|
||||
- "8998:17442"
|
||||
image: tzahi12345/youtubedl-material:latest
|
||||
ytdl-mongo-db:
|
||||
# If you are using a Raspberry Pi, use mongo:4.4.18
|
||||
image: mongo:4
|
||||
logging:
|
||||
driver: "none"
|
||||
|
||||
@@ -3,13 +3,26 @@ import requests
|
||||
import shutil
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
from github import Github
|
||||
|
||||
machine = platform.machine()
|
||||
|
||||
def isARM():
|
||||
return True if machine.startswith('arm') else False
|
||||
# https://stackoverflow.com/questions/45125516/possible-values-for-uname-m
|
||||
MACHINES_TO_ZIP = OrderedDict([
|
||||
("x86_64", "Linux-x64"),
|
||||
("aarch64", "LinuxArm64"),
|
||||
("armv8", "LinuxArm64"),
|
||||
("arm", "LinuxArm"),
|
||||
("AMD64", "Windows-x64")
|
||||
])
|
||||
|
||||
def getZipName():
|
||||
for possibleMachine, possibleZipName in MACHINES_TO_ZIP.items():
|
||||
if possibleMachine in machine:
|
||||
return possibleZipName
|
||||
|
||||
def getLatestFileInRepo(repo, search_string):
|
||||
# Create an unauthenticated instance of the Github object
|
||||
@@ -46,8 +59,11 @@ def getLatestFileInRepo(repo, search_string):
|
||||
print(f'No release found with {search_string}')
|
||||
|
||||
def getLatestCLIRelease():
|
||||
isArm = isARM()
|
||||
searchString = r'.*CLI.*' + "LinuxArm.zip" if isArm else "Linux-x64.zip"
|
||||
zipName = getZipName()
|
||||
if not zipName:
|
||||
print(f"GetTwitchDownloader.py could not get valid path for '{machine}'. Exiting...")
|
||||
sys.exit(1)
|
||||
searchString = r'.*CLI.*' + zipName
|
||||
getLatestFileInRepo("lay295/TwitchDownloader", searchString)
|
||||
|
||||
getLatestCLIRelease()
|
||||
|
||||
821
package-lock.json
generated
821
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,18 +34,17 @@
|
||||
"@angular/platform-browser-dynamic": "^15.0.1",
|
||||
"@angular/router": "^15.0.1",
|
||||
"@fontsource/material-icons": "^4.5.4",
|
||||
"@ngneat/content-loader": "^5.0.0",
|
||||
"@ngneat/content-loader": "^7.0.0",
|
||||
"@videogular/ngx-videogular": "^6.0.0",
|
||||
"core-js": "^2.4.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"filesize": "^10.0.7",
|
||||
"fingerprintjs2": "^2.1.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"material-icons": "^1.10.8",
|
||||
"nan": "^2.14.1",
|
||||
"ngx-avatars": "^1.4.1",
|
||||
"ngx-file-drop": "^13.0.0",
|
||||
"ngx-file-drop": "^15.0.0",
|
||||
"rxjs": "^6.6.3",
|
||||
"rxjs-compat": "^6.6.7",
|
||||
"tslib": "^2.0.0",
|
||||
@@ -66,7 +65,6 @@
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"ajv": "^7.2.4",
|
||||
"codelyzer": "^6.0.0",
|
||||
"electron": "^19.1.9",
|
||||
"eslint": "^7.32.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<mat-icon>person</mat-icon>
|
||||
<span i18n="Profile menu label">Profile</span>
|
||||
</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>
|
||||
<span i18n="Archives menu label">Archives</span>
|
||||
</button>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
||||
<mat-selection-list *ngIf="!normal_files_received">
|
||||
<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-selection-list>
|
||||
</ng-container>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<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>
|
||||
</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 -->
|
||||
<div style="visibility: hidden; position: fixed"
|
||||
[style.left]="contextMenuPosition.x"
|
||||
@@ -35,14 +35,9 @@
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
<mat-divider></mat-divider>
|
||||
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item>
|
||||
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
|
||||
</button>
|
||||
<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>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</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 *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>
|
||||
@@ -68,11 +63,11 @@
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,6 @@ export class MainComponent implements OnInit {
|
||||
allowQualitySelect = false;
|
||||
downloadOnlyMode = false;
|
||||
forceAutoplay = false;
|
||||
use_youtubedl_archive = false;
|
||||
globalCustomArgs = null;
|
||||
allowAdvancedDownload = false;
|
||||
useDefaultDownloadingAgent = true;
|
||||
@@ -188,7 +187,6 @@ export class MainComponent implements OnInit {
|
||||
&& this.postsService.hasPermission('filemanager');
|
||||
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
|
||||
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.youtubeSearchEnabled = this.postsService.config['API'] && this.postsService.config['API']['use_youtube_API'] &&
|
||||
this.postsService.config['API']['youtube_API_key'];
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import * as Fingerprint2 from 'fingerprintjs2';
|
||||
import {
|
||||
ChangeRolePermissionsRequest,
|
||||
ChangeUserPermissionsRequest,
|
||||
@@ -131,7 +130,6 @@ export class PostsService implements CanActivate {
|
||||
|
||||
// auth
|
||||
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
||||
session_id = null;
|
||||
httpOptions: {
|
||||
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');
|
||||
|
||||
// get config
|
||||
@@ -796,7 +788,7 @@ export class PostsService implements CanActivate {
|
||||
|
||||
resetHttpParams() {
|
||||
// resets http params
|
||||
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
|
||||
this.http_params = `apiKey=${this.auth_token}`
|
||||
|
||||
this.httpOptions = {
|
||||
params: new HttpParams({
|
||||
|
||||
@@ -387,9 +387,16 @@
|
||||
</div>
|
||||
<div class="col-12 mb-2 mt-3">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<mat-label i18n="Discord webhook URL">Discord Webhook URL</mat-label>
|
||||
<mat-label i18n="Discord Webhook URL">Discord Webhook URL</mat-label>
|
||||
<input placeholder="https://discord.com/api/webhooks/<webhook_id>/<webhook_token>" [(ngModel)]="new_config['API']['discord_webhook_URL']" matInput>
|
||||
<mat-hint><a target="_blank" href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"><ng-container i18n="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>
|
||||
</div>
|
||||
<div class="col-12 mt-3">
|
||||
|
||||
@@ -443,7 +443,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
|
||||
@@ -463,7 +463,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Cancel</note>
|
||||
</trans-unit>
|
||||
@@ -905,7 +905,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Close</note>
|
||||
</trans-unit>
|
||||
@@ -1082,7 +1082,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
|
||||
@@ -1090,11 +1090,11 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">save user edit action button tooltip</note>
|
||||
</trans-unit>
|
||||
@@ -1296,44 +1296,44 @@
|
||||
<source>Delete success!</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8348223454028662277" datatype="html">
|
||||
<source>OK.</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7405156667148936748" datatype="html">
|
||||
<source>Delete failed!</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8937901770314883418" datatype="html">
|
||||
<source>Successfully deleted file: </source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="ddc31f2885b1b33a7651963254b0c197f2a64086" datatype="html">
|
||||
@@ -1758,39 +1758,35 @@
|
||||
<source>Delete and redownload</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">39</context>
|
||||
<context context-type="linenumber">38</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Delete and redownload subscription video button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="e58f5716d6c08b6a841eb003c9f9774b5c5d34a9" datatype="html">
|
||||
<source>Delete and don't download again</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
|
||||
<context context-type="linenumber">42</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Delete forever subscription video button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
|
||||
<source>Delete</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">44</context>
|
||||
<context context-type="linenumber">39</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">50</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Delete video button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="e58f5716d6c08b6a841eb003c9f9774b5c5d34a9" datatype="html">
|
||||
<source>Delete and don't download again</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
|
||||
<context context-type="linenumber">40</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Delete and don't download again</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
|
||||
<source>Edit</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">48</context>
|
||||
<context context-type="linenumber">43</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
|
||||
@@ -1965,11 +1961,11 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">About bug click here</note>
|
||||
</trans-unit>
|
||||
@@ -2127,7 +2123,7 @@
|
||||
<source>Add new rule</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Add new rule tooltip</note>
|
||||
</trans-unit>
|
||||
@@ -2135,7 +2131,7 @@
|
||||
<source>Custom file output</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
|
||||
@@ -2151,7 +2147,7 @@
|
||||
<source>Documentation</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
|
||||
@@ -2171,7 +2167,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Custom output template documentation link</note>
|
||||
</trans-unit>
|
||||
@@ -2179,7 +2175,7 @@
|
||||
<source>Path is relative to the config download path. Don't include extension.</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html</context>
|
||||
@@ -2319,7 +2315,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Generate RSS URL</note>
|
||||
</trans-unit>
|
||||
@@ -2803,7 +2799,7 @@
|
||||
<source>View count</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">View count</note>
|
||||
</trans-unit>
|
||||
@@ -2811,7 +2807,7 @@
|
||||
<source>Local view count</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Local view count</note>
|
||||
</trans-unit>
|
||||
@@ -2819,7 +2815,7 @@
|
||||
<source>Resolution:</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Video resolution property</note>
|
||||
</trans-unit>
|
||||
@@ -2827,7 +2823,7 @@
|
||||
<source>Audio bitrate:</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Video audio bitrate property</note>
|
||||
</trans-unit>
|
||||
@@ -2835,7 +2831,7 @@
|
||||
<source>File size:</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Video file size property</note>
|
||||
</trans-unit>
|
||||
@@ -2843,7 +2839,7 @@
|
||||
<source>Path:</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Video path property</note>
|
||||
</trans-unit>
|
||||
@@ -3011,18 +3007,18 @@
|
||||
<source>Download failed!</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7220285196408439810" datatype="html">
|
||||
<source>Download for <x id="url" equiv-text="url"/> has been queued!</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="dad95154dcef3509b8cc705046061fd24994bbb7" datatype="html">
|
||||
@@ -3475,51 +3471,19 @@
|
||||
</context-group>
|
||||
<note priority="1" from="description">Youtube API Key setting hint</note>
|
||||
</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">
|
||||
<source>Auto-download Twitch Chat</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Auto download Twitch Chat setting</note>
|
||||
</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">
|
||||
<source>Enables a button to skip ads when viewing supported videos.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">SponsorBlock API tooltip</note>
|
||||
</trans-unit>
|
||||
@@ -3527,7 +3491,7 @@
|
||||
<source>Use SponsorBlock API</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Use SponsorBlock API setting</note>
|
||||
</trans-unit>
|
||||
@@ -3535,7 +3499,7 @@
|
||||
<source>Generates NFO files with every download, primarily used by Kodi.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Generate NFO files tooltip</note>
|
||||
</trans-unit>
|
||||
@@ -3543,7 +3507,7 @@
|
||||
<source>Generate NFO files</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Generate NFO files setting</note>
|
||||
</trans-unit>
|
||||
@@ -3551,7 +3515,7 @@
|
||||
<source>Enable RSS Feed</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Enable RSS Feed setting</note>
|
||||
</trans-unit>
|
||||
@@ -3559,7 +3523,7 @@
|
||||
<source>Be careful enabling this with multi-user mode! User data may be exposed.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">RSS Feed prefix</note>
|
||||
</trans-unit>
|
||||
@@ -3567,7 +3531,7 @@
|
||||
<source>See documentation here.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">RSS feed documentation</note>
|
||||
</trans-unit>
|
||||
@@ -3575,7 +3539,7 @@
|
||||
<source>to download the official YoutubeDL-Material Chrome extension manually.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Chrome click here suffix</note>
|
||||
</trans-unit>
|
||||
@@ -3583,7 +3547,7 @@
|
||||
<source>You must manually load the extension and modify the extension's settings to set the frontend URL.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Chrome setup suffix</note>
|
||||
</trans-unit>
|
||||
@@ -3591,7 +3555,7 @@
|
||||
<source>to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Firefox click here suffix</note>
|
||||
</trans-unit>
|
||||
@@ -3599,7 +3563,7 @@
|
||||
<source>Detailed setup instructions.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Firefox setup prefix link</note>
|
||||
</trans-unit>
|
||||
@@ -3607,7 +3571,7 @@
|
||||
<source>Not much is required other than changing the extension's settings to set the frontend URL.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Firefox setup suffix</note>
|
||||
</trans-unit>
|
||||
@@ -3615,7 +3579,7 @@
|
||||
<source>Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Bookmarklet instructions</note>
|
||||
</trans-unit>
|
||||
@@ -3623,7 +3587,7 @@
|
||||
<source>Generate 'audio only' bookmarklet</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Generate audio only bookmarklet checkbox</note>
|
||||
</trans-unit>
|
||||
@@ -3631,7 +3595,7 @@
|
||||
<source>Database</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Database settings label</note>
|
||||
</trans-unit>
|
||||
@@ -3639,7 +3603,7 @@
|
||||
<source>Database location:</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Database location label</note>
|
||||
</trans-unit>
|
||||
@@ -3647,7 +3611,7 @@
|
||||
<source>Records per table</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Records per table label</note>
|
||||
</trans-unit>
|
||||
@@ -3655,7 +3619,7 @@
|
||||
<source>MongoDB Connection String</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">MongoDB Connection String</note>
|
||||
</trans-unit>
|
||||
@@ -3663,7 +3627,7 @@
|
||||
<source>Example:</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">MongoDB Connection String setting hint AKA preamble</note>
|
||||
</trans-unit>
|
||||
@@ -3671,7 +3635,7 @@
|
||||
<source>Test connection string</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Test connection string button</note>
|
||||
</trans-unit>
|
||||
@@ -3679,7 +3643,7 @@
|
||||
<source>Transfer DB to </source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Transfer DB button</note>
|
||||
</trans-unit>
|
||||
@@ -3687,7 +3651,7 @@
|
||||
<source>Database information could not be retrieved. Check the server logs for more information.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Database info not retrieved error message</note>
|
||||
</trans-unit>
|
||||
@@ -3695,7 +3659,7 @@
|
||||
<source>Notifications</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Notifications settings label</note>
|
||||
</trans-unit>
|
||||
@@ -3703,7 +3667,7 @@
|
||||
<source>Enable notifications</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Enable notifications setting</note>
|
||||
</trans-unit>
|
||||
@@ -3711,7 +3675,7 @@
|
||||
<source>Enable all notifications</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Enable all notifications setting</note>
|
||||
</trans-unit>
|
||||
@@ -3719,7 +3683,7 @@
|
||||
<source>Allowed notification types</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Allowed notification types</note>
|
||||
</trans-unit>
|
||||
@@ -3727,7 +3691,7 @@
|
||||
<source>Download complete</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Download complete</note>
|
||||
</trans-unit>
|
||||
@@ -3735,7 +3699,7 @@
|
||||
<source>Download error</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Download error</note>
|
||||
</trans-unit>
|
||||
@@ -3743,7 +3707,7 @@
|
||||
<source>Task finished</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Task finished</note>
|
||||
</trans-unit>
|
||||
@@ -3751,15 +3715,55 @@
|
||||
<source>Webhook URL</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">webhook URL</note>
|
||||
</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">
|
||||
<source>Use ntfy API</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Use ntfy API setting</note>
|
||||
</trans-unit>
|
||||
@@ -3767,31 +3771,15 @@
|
||||
<source>ntfy topic URL</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">ntfy topic 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">411</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||
<context context-type="linenumber">421</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||
<context context-type="linenumber">428</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">ntfy API setting hint</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html">
|
||||
<source>Use gotify API</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Use gotify API setting</note>
|
||||
</trans-unit>
|
||||
@@ -3799,7 +3787,7 @@
|
||||
<source>Gotify server URL</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Gotify server URL</note>
|
||||
</trans-unit>
|
||||
@@ -3807,7 +3795,7 @@
|
||||
<source>Gotify app token</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Gotify app token</note>
|
||||
</trans-unit>
|
||||
@@ -3815,7 +3803,7 @@
|
||||
<source>Use Telegram API</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Use Telegram API setting</note>
|
||||
</trans-unit>
|
||||
@@ -3823,7 +3811,7 @@
|
||||
<source>Telegram bot token</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Telegram bot token</note>
|
||||
</trans-unit>
|
||||
@@ -3831,7 +3819,7 @@
|
||||
<source>Create bot here.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Telegram bot create link</note>
|
||||
</trans-unit>
|
||||
@@ -3839,7 +3827,7 @@
|
||||
<source>Telegram chat ID</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Telegram chat ID</note>
|
||||
</trans-unit>
|
||||
@@ -3847,7 +3835,7 @@
|
||||
<source>How do I get the chat ID?</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Telegram chat ID help</note>
|
||||
</trans-unit>
|
||||
@@ -3855,7 +3843,7 @@
|
||||
<source>Advanced</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Host settings label</note>
|
||||
</trans-unit>
|
||||
@@ -3863,7 +3851,7 @@
|
||||
<source>Select a downloader</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Default downloader select label</note>
|
||||
</trans-unit>
|
||||
@@ -3871,7 +3859,7 @@
|
||||
<source>Restart required.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Restart required hint</note>
|
||||
</trans-unit>
|
||||
@@ -3879,7 +3867,7 @@
|
||||
<source>Use default downloading agent</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Use default downloading agent setting</note>
|
||||
</trans-unit>
|
||||
@@ -3887,7 +3875,7 @@
|
||||
<source>Select a download agent</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Custom downloader select label</note>
|
||||
</trans-unit>
|
||||
@@ -3895,7 +3883,7 @@
|
||||
<source>Log Level</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Log Level label</note>
|
||||
</trans-unit>
|
||||
@@ -3903,7 +3891,7 @@
|
||||
<source>Login expiration</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Login expiration select label</note>
|
||||
</trans-unit>
|
||||
@@ -3911,7 +3899,7 @@
|
||||
<source>Allow advanced download</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Allow advanced downloading setting</note>
|
||||
</trans-unit>
|
||||
@@ -3919,7 +3907,7 @@
|
||||
<source>Use Cookies</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Use cookies setting</note>
|
||||
</trans-unit>
|
||||
@@ -3927,7 +3915,7 @@
|
||||
<source>Set Cookies</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Set cookies button</note>
|
||||
</trans-unit>
|
||||
@@ -3935,7 +3923,7 @@
|
||||
<source>Restart server</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Restart server button</note>
|
||||
</trans-unit>
|
||||
@@ -3943,7 +3931,7 @@
|
||||
<source>Users</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Users settings label</note>
|
||||
</trans-unit>
|
||||
@@ -3951,7 +3939,7 @@
|
||||
<source>Allow user registration</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Allow registration setting</note>
|
||||
</trans-unit>
|
||||
@@ -3959,7 +3947,7 @@
|
||||
<source>Auth method</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Auth method</note>
|
||||
</trans-unit>
|
||||
@@ -3967,7 +3955,7 @@
|
||||
<source>Internal</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Internal auth method</note>
|
||||
</trans-unit>
|
||||
@@ -3975,7 +3963,7 @@
|
||||
<source>LDAP</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">LDAP auth method</note>
|
||||
</trans-unit>
|
||||
@@ -3983,7 +3971,7 @@
|
||||
<source>LDAP URL</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">LDAP URL</note>
|
||||
</trans-unit>
|
||||
@@ -3991,7 +3979,7 @@
|
||||
<source>Bind DN</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Bind DN</note>
|
||||
</trans-unit>
|
||||
@@ -3999,7 +3987,7 @@
|
||||
<source>Bind Credentials</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Bind Credentials</note>
|
||||
</trans-unit>
|
||||
@@ -4007,7 +3995,7 @@
|
||||
<source>Search Base</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Search Base</note>
|
||||
</trans-unit>
|
||||
@@ -4015,7 +4003,7 @@
|
||||
<source>Search Filter</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Search Filter</note>
|
||||
</trans-unit>
|
||||
@@ -4023,7 +4011,7 @@
|
||||
<source>Logs</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<note priority="1" from="description">Logs settings label</note>
|
||||
</trans-unit>
|
||||
|
||||
Reference in New Issue
Block a user