Compare commits

..

30 Commits

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

View File

@@ -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

View File

@@ -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 }}"}'

View File

@@ -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 }}

View File

@@ -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
View File

@@ -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
View File

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

View File

@@ -37,6 +37,8 @@ COPY [ "src/", "/build/src/" ]
RUN npm install && \
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

View File

@@ -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`.

View File

@@ -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',

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

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

View File

@@ -64,6 +64,9 @@ exports.sendNotification = async (notification) => {
if (config_api.getConfigItem('ytdl_discord_webhook_url')) {
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}`);

View File

@@ -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",

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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']));
}
}
}

View File

@@ -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);
// });

View File

@@ -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}`);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

@@ -18,6 +18,7 @@ services:
- "8998:17442"
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"

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'];

View File

@@ -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({

View File

@@ -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">

View File

@@ -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&apos;t download again</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">45</context>
</context-group>
<note priority="1" from="description">Delete forever subscription video button</note>
</trans-unit>
<trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
<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&apos;t download again</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
<note priority="1" from="description">Delete and don&apos;t download again</note>
</trans-unit>
<trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
<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&apos;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&apos;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&apos;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&apos;re good to go! Just navigate to the YouTube video you&apos;d like to download, and click the bookmark.</source>
<context-group purpose="location">
<context 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 &apos;audio only&apos; 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>