mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-30 00:20:57 +03:00
Compare commits
13 Commits
slack-noti
...
desktop-ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a608d8b7a | ||
|
|
8e1ebb5a4a | ||
|
|
bf38d9e67e | ||
|
|
a7c041aae1 | ||
|
|
a2ae9db1c6 | ||
|
|
4aa98916ed | ||
|
|
2098cc542c | ||
|
|
26de55fe86 | ||
|
|
08c5647521 | ||
|
|
a7507aa803 | ||
|
|
0c2937695c | ||
|
|
4fd676d50c | ||
|
|
771fe3d985 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -67,6 +67,7 @@ backend/appdata/users.json
|
|||||||
backend/users/*
|
backend/users/*
|
||||||
backend/appdata/cookies.txt
|
backend/appdata/cookies.txt
|
||||||
backend/public
|
backend/public
|
||||||
|
backend/dist
|
||||||
src/assets/i18n/*.json
|
src/assets/i18n/*.json
|
||||||
|
|
||||||
# User Files
|
# User Files
|
||||||
|
|||||||
13
Dockerfile
13
Dockerfile
@@ -2,7 +2,7 @@
|
|||||||
FROM ubuntu:22.04 AS ffmpeg
|
FROM ubuntu:22.04 AS ffmpeg
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
# Use script due local build compability
|
# Use script due local build compability
|
||||||
COPY docker-utils/ffmpeg-fetch.sh .
|
COPY ffmpeg-fetch.sh .
|
||||||
RUN chmod +x ffmpeg-fetch.sh
|
RUN chmod +x ffmpeg-fetch.sh
|
||||||
RUN sh ./ffmpeg-fetch.sh
|
RUN sh ./ffmpeg-fetch.sh
|
||||||
|
|
||||||
@@ -47,15 +47,6 @@ RUN npm config set strict-ssl false && \
|
|||||||
npm install --prod && \
|
npm install --prod && \
|
||||||
ls -al
|
ls -al
|
||||||
|
|
||||||
FROM base as python
|
|
||||||
WORKDIR /app
|
|
||||||
COPY docker-utils/GetTwitchDownloader.py .
|
|
||||||
RUN apt update && \
|
|
||||||
apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip && \
|
|
||||||
apt clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
RUN pip install PyGithub requests
|
|
||||||
RUN python GetTwitchDownloader.py
|
|
||||||
|
|
||||||
# Final image
|
# Final image
|
||||||
FROM base
|
FROM base
|
||||||
@@ -64,7 +55,7 @@ RUN npm install -g pm2 && \
|
|||||||
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
|
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
|
||||||
apt clean && \
|
apt clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
RUN pip install pycryptodomex
|
RUN pip install tdh-tcd pycryptodomex
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# User 1000 already exist from base image
|
# User 1000 already exist from base image
|
||||||
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
|
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
|
||||||
|
|||||||
118
backend/app.js
118
backend/app.js
@@ -1,3 +1,7 @@
|
|||||||
|
// TODO: ignore this if not in electron
|
||||||
|
const rootPath = require('electron-root-path').rootPath;
|
||||||
|
process.chdir(rootPath);
|
||||||
|
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
@@ -56,48 +60,7 @@ let debugMode = process.env.YTDL_MODE === 'debug';
|
|||||||
|
|
||||||
const admin_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
const admin_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
||||||
|
|
||||||
// logging setup
|
// required initialization
|
||||||
|
|
||||||
config_api.initialize();
|
|
||||||
db_api.initialize(db, users_db);
|
|
||||||
auth_api.initialize(db_api);
|
|
||||||
|
|
||||||
// Set some defaults
|
|
||||||
db.defaults(
|
|
||||||
{
|
|
||||||
playlists: [],
|
|
||||||
files: [],
|
|
||||||
configWriteFlag: false,
|
|
||||||
downloads: {},
|
|
||||||
subscriptions: [],
|
|
||||||
files_to_db_migration_complete: false,
|
|
||||||
tasks_manager_role_migration_complete: false,
|
|
||||||
archives_migration_complete: false
|
|
||||||
}).write();
|
|
||||||
|
|
||||||
users_db.defaults(
|
|
||||||
{
|
|
||||||
users: [],
|
|
||||||
roles: {
|
|
||||||
"admin": {
|
|
||||||
"permissions": [
|
|
||||||
'filemanager',
|
|
||||||
'settings',
|
|
||||||
'subscriptions',
|
|
||||||
'sharing',
|
|
||||||
'advanced_download',
|
|
||||||
'downloads_manager'
|
|
||||||
]
|
|
||||||
}, "user": {
|
|
||||||
"permissions": [
|
|
||||||
'filemanager',
|
|
||||||
'subscriptions',
|
|
||||||
'sharing'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).write();
|
|
||||||
|
|
||||||
// config values
|
// config values
|
||||||
let url = null;
|
let url = null;
|
||||||
@@ -149,20 +112,61 @@ if (fs.existsSync('version.json')) {
|
|||||||
version_info = {'type': 'N/A', 'tag': 'N/A', 'commit': 'N/A', 'date': 'N/A'};
|
version_info = {'type': 'N/A', 'tag': 'N/A', 'commit': 'N/A', 'date': 'N/A'};
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't overwrite config if it already happened.. NOT
|
exports.initialize = () => {
|
||||||
// let alreadyWritten = db.get('configWriteFlag').value();
|
config_api.initialize();
|
||||||
|
db_api.initialize(db, users_db);
|
||||||
|
auth_api.initialize(db_api);
|
||||||
|
|
||||||
// checks if config exists, if not, a config is auto generated
|
// Set some defaults
|
||||||
config_api.configExistsCheck();
|
db.defaults(
|
||||||
|
{
|
||||||
|
playlists: [],
|
||||||
|
files: [],
|
||||||
|
configWriteFlag: false,
|
||||||
|
downloads: {},
|
||||||
|
subscriptions: [],
|
||||||
|
files_to_db_migration_complete: false,
|
||||||
|
tasks_manager_role_migration_complete: false,
|
||||||
|
archives_migration_complete: false
|
||||||
|
}).write();
|
||||||
|
|
||||||
setAndLoadConfig();
|
users_db.defaults(
|
||||||
|
{
|
||||||
|
users: [],
|
||||||
|
roles: {
|
||||||
|
"admin": {
|
||||||
|
"permissions": [
|
||||||
|
'filemanager',
|
||||||
|
'settings',
|
||||||
|
'subscriptions',
|
||||||
|
'sharing',
|
||||||
|
'advanced_download',
|
||||||
|
'downloads_manager'
|
||||||
|
]
|
||||||
|
}, "user": {
|
||||||
|
"permissions": [
|
||||||
|
'filemanager',
|
||||||
|
'subscriptions',
|
||||||
|
'sharing'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).write();
|
||||||
|
|
||||||
app.use(bodyParser.urlencoded({ extended: false }));
|
// checks if config exists, if not, a config is auto generated
|
||||||
app.use(bodyParser.json());
|
config_api.configExistsCheck();
|
||||||
|
|
||||||
// use passport
|
setAndLoadConfig();
|
||||||
app.use(auth_api.passport.initialize());
|
|
||||||
app.use(auth_api.passport.session());
|
app.use(bodyParser.urlencoded({ extended: false }));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
// use passport
|
||||||
|
app.use(auth_api.passport.initialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.initialize();
|
||||||
|
|
||||||
// actual functions
|
// actual functions
|
||||||
|
|
||||||
@@ -252,7 +256,7 @@ async function simplifyDBFileStructure() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startServer() {
|
exports.startServer = async () => {
|
||||||
if (process.env.USING_HEROKU && process.env.PORT) {
|
if (process.env.USING_HEROKU && process.env.PORT) {
|
||||||
// default to heroku port if using heroku
|
// default to heroku port if using heroku
|
||||||
backendPort = process.env.PORT || backendPort;
|
backendPort = process.env.PORT || backendPort;
|
||||||
@@ -545,7 +549,9 @@ async function loadConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start the server here
|
// start the server here
|
||||||
startServer();
|
if (typeof require !== 'undefined' && require.main === module) {
|
||||||
|
exports.startServer();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1639,7 +1645,9 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
|
|||||||
else file_path = null;
|
else file_path = null;
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(file_path)) {
|
if (!fs.existsSync(file_path)) {
|
||||||
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
|
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj.id}`);
|
||||||
|
res.sendStatus(404);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const stat = fs.statSync(file_path);
|
const stat = fs.statSync(file_path);
|
||||||
const fileSize = stat.size;
|
const fileSize = stat.size;
|
||||||
|
|||||||
@@ -23,12 +23,7 @@
|
|||||||
"download_only_mode": false,
|
"download_only_mode": false,
|
||||||
"allow_autoplay": true,
|
"allow_autoplay": true,
|
||||||
"enable_downloads_manager": true,
|
"enable_downloads_manager": true,
|
||||||
"allow_playlist_categorization": true,
|
"allow_playlist_categorization": true
|
||||||
"force_autoplay": false,
|
|
||||||
"enable_notifications": true,
|
|
||||||
"enable_all_notifications": true,
|
|
||||||
"allowed_notification_types": [],
|
|
||||||
"enable_rss_feed": false
|
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"use_API_key": false,
|
"use_API_key": false,
|
||||||
@@ -40,17 +35,7 @@
|
|||||||
"twitch_client_secret": "",
|
"twitch_client_secret": "",
|
||||||
"twitch_auto_download_chat": false,
|
"twitch_auto_download_chat": false,
|
||||||
"use_sponsorblock_API": false,
|
"use_sponsorblock_API": false,
|
||||||
"generate_NFO_files": false,
|
"generate_NFO_files": false
|
||||||
"use_ntfy_API": false,
|
|
||||||
"ntfy_topic_URL": "",
|
|
||||||
"use_gotify_API": false,
|
|
||||||
"gotify_server_URL": "",
|
|
||||||
"gotify_app_token": "",
|
|
||||||
"use_telegram_API": false,
|
|
||||||
"telegram_bot_token": "",
|
|
||||||
"telegram_chat_id": "",
|
|
||||||
"webhook_URL": "",
|
|
||||||
"discord_webhook_URL": ""
|
|
||||||
},
|
},
|
||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
|
|||||||
@@ -208,6 +208,9 @@ const DEFAULT_CONFIG = {
|
|||||||
"API_key": "",
|
"API_key": "",
|
||||||
"use_youtube_API": false,
|
"use_youtube_API": false,
|
||||||
"youtube_API_key": "",
|
"youtube_API_key": "",
|
||||||
|
"use_twitch_API": false,
|
||||||
|
"twitch_client_ID": "",
|
||||||
|
"twitch_client_secret": "",
|
||||||
"twitch_auto_download_chat": false,
|
"twitch_auto_download_chat": false,
|
||||||
"use_sponsorblock_API": false,
|
"use_sponsorblock_API": false,
|
||||||
"generate_NFO_files": false,
|
"generate_NFO_files": false,
|
||||||
@@ -219,9 +222,7 @@ const DEFAULT_CONFIG = {
|
|||||||
"use_telegram_API": false,
|
"use_telegram_API": false,
|
||||||
"telegram_bot_token": "",
|
"telegram_bot_token": "",
|
||||||
"telegram_chat_id": "",
|
"telegram_chat_id": "",
|
||||||
"webhook_URL": "",
|
"webhook_URL": ""
|
||||||
"discord_webhook_URL": "",
|
|
||||||
"slack_webhook_URL": "",
|
|
||||||
},
|
},
|
||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
|
|||||||
@@ -110,6 +110,18 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_youtube_api_key',
|
'key': 'ytdl_youtube_api_key',
|
||||||
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
||||||
},
|
},
|
||||||
|
'ytdl_use_twitch_api': {
|
||||||
|
'key': 'ytdl_use_twitch_api',
|
||||||
|
'path': 'YoutubeDLMaterial.API.use_twitch_API'
|
||||||
|
},
|
||||||
|
'ytdl_twitch_client_id': {
|
||||||
|
'key': 'ytdl_twitch_client_id',
|
||||||
|
'path': 'YoutubeDLMaterial.API.twitch_client_ID'
|
||||||
|
},
|
||||||
|
'ytdl_twitch_client_secret': {
|
||||||
|
'key': 'ytdl_twitch_client_secret',
|
||||||
|
'path': 'YoutubeDLMaterial.API.twitch_client_secret'
|
||||||
|
},
|
||||||
'ytdl_twitch_auto_download_chat': {
|
'ytdl_twitch_auto_download_chat': {
|
||||||
'key': 'ytdl_twitch_auto_download_chat',
|
'key': 'ytdl_twitch_auto_download_chat',
|
||||||
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
||||||
@@ -158,14 +170,6 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_webhook_url',
|
'key': 'ytdl_webhook_url',
|
||||||
'path': 'YoutubeDLMaterial.API.webhook_URL'
|
'path': 'YoutubeDLMaterial.API.webhook_URL'
|
||||||
},
|
},
|
||||||
'ytdl_discord_webhook_url': {
|
|
||||||
'key': 'ytdl_discord_webhook_url',
|
|
||||||
'path': 'YoutubeDLMaterial.API.discord_webhook_URL'
|
|
||||||
},
|
|
||||||
'ytdl_slack_webhook_url': {
|
|
||||||
'key': 'ytdl_slack_webhook_url',
|
|
||||||
'path': 'YoutubeDLMaterial.API.slack_webhook_URL'
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
// Themes
|
// Themes
|
||||||
@@ -350,6 +354,4 @@ const YTDL_ARGS_WITH_VALUES = [
|
|||||||
// we're using a Set here for performance
|
// we're using a Set here for performance
|
||||||
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
||||||
|
|
||||||
exports.ICON_URL = 'https://i.imgur.com/IKOlr0N.png';
|
|
||||||
|
|
||||||
exports.CURRENT_VERSION = 'v4.3.1';
|
exports.CURRENT_VERSION = 'v4.3.1';
|
||||||
|
|||||||
@@ -720,14 +720,7 @@ exports.updateRecord = async (table, filter_obj, update_obj, nested_mode = false
|
|||||||
exports.updateRecords = async (table, filter_obj, update_obj) => {
|
exports.updateRecords = async (table, filter_obj, update_obj) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').each((record) => {
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write();
|
||||||
const props_to_update = Object.keys(update_obj);
|
|
||||||
for (let i = 0; i < props_to_update.length; i++) {
|
|
||||||
const prop_to_update = props_to_update[i];
|
|
||||||
const prop_value = update_obj[prop_to_update];
|
|
||||||
record[prop_to_update] = prop_value;
|
|
||||||
}
|
|
||||||
}).write();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ exports.clearDownload = async (download_uid) => {
|
|||||||
|
|
||||||
async function handleDownloadError(download, error_message, error_type = null) {
|
async function handleDownloadError(download, error_message, error_type = null) {
|
||||||
if (!download || !download['uid']) return;
|
if (!download || !download['uid']) return;
|
||||||
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_message, error_type);
|
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_type);
|
||||||
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
|
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,10 +245,11 @@ async function collectInfo(download_uid) {
|
|||||||
options.customOutput = category['custom_output'];
|
options.customOutput = category['custom_output'];
|
||||||
options.noRelativePath = true;
|
options.noRelativePath = true;
|
||||||
args = await exports.generateArgs(url, type, options, download['user_uid']);
|
args = await exports.generateArgs(url, type, options, download['user_uid']);
|
||||||
|
args = utils.filterArgs(args, ['--no-simulate']);
|
||||||
info = await exports.getVideoInfoByURL(url, args, download_uid);
|
info = await exports.getVideoInfoByURL(url, args, download_uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripped_category = category ? {name: category['name'], uid: category['uid']} : null;
|
download['category'] = category;
|
||||||
|
|
||||||
// setup info required to calculate download progress
|
// setup info required to calculate download progress
|
||||||
|
|
||||||
@@ -271,7 +272,6 @@ async function collectInfo(download_uid) {
|
|||||||
files_to_check_for_progress: files_to_check_for_progress,
|
files_to_check_for_progress: files_to_check_for_progress,
|
||||||
expected_file_size: expected_file_size,
|
expected_file_size: expected_file_size,
|
||||||
title: playlist_title ? playlist_title : info['title'],
|
title: playlist_title ? playlist_title : info['title'],
|
||||||
category: stripped_category,
|
|
||||||
prefetched_info: null
|
prefetched_info: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -314,7 +314,7 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
clearInterval(download_checker);
|
clearInterval(download_checker);
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error(err.stderr);
|
logger.error(err.stderr);
|
||||||
await handleDownloadError(download, err.stderr, 'unknown_error');
|
await handleDownloadError(download, err.stderr);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
} else if (output) {
|
} else if (output) {
|
||||||
@@ -350,7 +350,7 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
||||||
|
|
||||||
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||||
&& config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||||
let vodId = url.split('twitch.tv/videos/')[1];
|
let vodId = url.split('twitch.tv/videos/')[1];
|
||||||
vodId = vodId.split('?')[0];
|
vodId = vodId.split('?')[0];
|
||||||
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
||||||
@@ -552,8 +552,7 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
// remove bad args
|
// remove bad args
|
||||||
const temp_args = utils.filterArgs(args, ['--no-simulate']);
|
const new_args = [...args];
|
||||||
const new_args = [...temp_args];
|
|
||||||
|
|
||||||
const archiveArgIndex = new_args.indexOf('--download-archive');
|
const archiveArgIndex = new_args.indexOf('--download-archive');
|
||||||
if (archiveArgIndex !== -1) {
|
if (archiveArgIndex !== -1) {
|
||||||
@@ -596,7 +595,7 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
|||||||
logger.error(error_message);
|
logger.error(error_message);
|
||||||
if (download_uid) {
|
if (download_uid) {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
await handleDownloadError(download, error_message, 'info_retrieve_failed');
|
await handleDownloadError(download, error_message);
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
|
|||||||
82
backend/main.js
Normal file
82
backend/main.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
const { app, BrowserWindow } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const elogger = require('electron-log');
|
||||||
|
const server = require('./app');
|
||||||
|
|
||||||
|
let win;
|
||||||
|
let splashWindow;
|
||||||
|
|
||||||
|
async function createSplashWindow() {
|
||||||
|
splashWindow = new BrowserWindow({
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
frame: false,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await splashWindow.loadFile('public/assets/splash.html')
|
||||||
|
splashWindow.on('closed', () => {
|
||||||
|
splashWindow = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMainWindow() {
|
||||||
|
win = new BrowserWindow(
|
||||||
|
{
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js')
|
||||||
|
},
|
||||||
|
icon: path.join(__dirname, 'favicon.ico'),
|
||||||
|
show: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// The following is optional and will open the DevTools:
|
||||||
|
// win.webContents.openDevTools()
|
||||||
|
|
||||||
|
win.on('closed', () => {
|
||||||
|
win = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPage() {
|
||||||
|
splashWindow.close()
|
||||||
|
// load the dist folder from Angular
|
||||||
|
win.loadURL('http://localhost:17442')
|
||||||
|
win.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWindow() {
|
||||||
|
await createSplashWindow();
|
||||||
|
elogger.info('Spawning server.')
|
||||||
|
// serverProcess = spawn('node', [path.join(__dirname, 'app.js')]);
|
||||||
|
await server.startServer();
|
||||||
|
elogger.info('Done spawning!')
|
||||||
|
createMainWindow();
|
||||||
|
loadPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on('ready', createWindow);
|
||||||
|
|
||||||
|
// on macOS, closing the window doesn't quit the app
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// initialize the app's main window
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (win === null) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
elogger.error(error.message);
|
||||||
|
});
|
||||||
@@ -2,16 +2,12 @@ const db_api = require('./db');
|
|||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const consts = require('./consts');
|
|
||||||
|
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
|
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const { gotify } = require("gotify");
|
const { gotify } = require("gotify");
|
||||||
const TelegramBot = require('node-telegram-bot-api');
|
const TelegramBot = require('node-telegram-bot-api');
|
||||||
const REST = require('@discordjs/rest').REST;
|
|
||||||
const API = require('@discordjs/core').API;
|
|
||||||
const EmbedBuilder = require('@discordjs/builders').EmbedBuilder;
|
|
||||||
|
|
||||||
const NOTIFICATION_TYPE_TO_TITLE = {
|
const NOTIFICATION_TYPE_TO_TITLE = {
|
||||||
task_finished: 'Task finished',
|
task_finished: 'Task finished',
|
||||||
@@ -22,7 +18,7 @@ const NOTIFICATION_TYPE_TO_TITLE = {
|
|||||||
const NOTIFICATION_TYPE_TO_BODY = {
|
const NOTIFICATION_TYPE_TO_BODY = {
|
||||||
task_finished: (notification) => notification['data']['task_title'],
|
task_finished: (notification) => notification['data']['task_title'],
|
||||||
download_complete: (notification) => {return `${notification['data']['file_title']}\nOriginal URL: ${notification['data']['original_url']}`},
|
download_complete: (notification) => {return `${notification['data']['file_title']}\nOriginal URL: ${notification['data']['original_url']}`},
|
||||||
download_error: (notification) => {return `Error: ${notification['data']['download_error_message']}\nError code: ${notification['data']['download_error_type']}\n\nOriginal URL: ${notification['data']['download_url']}`}
|
download_error: (notification) => {return `Error: ${notification['data']['download_error_type']}\nURL: ${notification['data']['download_url']}`}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOTIFICATION_TYPE_TO_URL = {
|
const NOTIFICATION_TYPE_TO_URL = {
|
||||||
@@ -61,12 +57,6 @@ exports.sendNotification = async (notification) => {
|
|||||||
if (config_api.getConfigItem('ytdl_webhook_url')) {
|
if (config_api.getConfigItem('ytdl_webhook_url')) {
|
||||||
sendGenericNotification(data);
|
sendGenericNotification(data);
|
||||||
}
|
}
|
||||||
if (config_api.getConfigItem('ytdl_discord_webhook_url')) {
|
|
||||||
sendDiscordNotification(data);
|
|
||||||
}
|
|
||||||
if (config_api.getConfigItem('ytdl_slack_webhook_url')) {
|
|
||||||
sendSlackNotification(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db_api.insertRecordIntoTable('notifications', notification);
|
await db_api.insertRecordIntoTable('notifications', notification);
|
||||||
return notification;
|
return notification;
|
||||||
@@ -89,9 +79,9 @@ exports.sendDownloadNotification = async (file, user_uid) => {
|
|||||||
return await exports.sendNotification(notification);
|
return await exports.sendNotification(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.sendDownloadErrorNotification = async (download, user_uid, error_message, error_type = null) => {
|
exports.sendDownloadErrorNotification = async (download, user_uid, error_type = null) => {
|
||||||
if (!notificationEnabled('download_error')) return;
|
if (!notificationEnabled('download_error')) return;
|
||||||
const data = {download_uid: download.uid, download_url: download.url, download_error_message: error_message, download_error_type: error_type};
|
const data = {download_uid: download.uid, download_url: download.url, download_error_type: error_type};
|
||||||
const notification = exports.createNotification('download_error', ['view_download_error', 'retry_download'], data, user_uid);
|
const notification = exports.createNotification('download_error', ['view_download_error', 'retry_download'], data, user_uid);
|
||||||
return await exports.sendNotification(notification);
|
return await exports.sendNotification(notification);
|
||||||
}
|
}
|
||||||
@@ -154,88 +144,6 @@ async function sendTelegramNotification({body, title, type, url, thumbnail}) {
|
|||||||
bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
|
bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
|
||||||
const discord_webhook_url = config_api.getConfigItem('ytdl_discord_webhook_url');
|
|
||||||
const url_split = discord_webhook_url.split('webhooks/');
|
|
||||||
const [webhook_id, webhook_token] = url_split[1].split('/');
|
|
||||||
const rest = new REST({ version: '10' });
|
|
||||||
const api = new API(rest);
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setTitle(title)
|
|
||||||
.setColor(0x00FFFF)
|
|
||||||
.setURL(url)
|
|
||||||
.setDescription(`ID: ${type}`);
|
|
||||||
if (thumbnail) embed.setThumbnail(thumbnail);
|
|
||||||
if (type === 'download_error') embed.setColor(0xFC2003);
|
|
||||||
|
|
||||||
const result = await api.webhooks.execute(webhook_id, webhook_token, {
|
|
||||||
content: body,
|
|
||||||
username: 'YoutubeDL-Material',
|
|
||||||
avatar_url: consts.ICON_URL,
|
|
||||||
embeds: [embed],
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendSlackNotification({body, title, type, url, thumbnail}) {
|
|
||||||
const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url');
|
|
||||||
logger.verbose(`Sending slack notification to ${slack_webhook_url}`);
|
|
||||||
const data = {
|
|
||||||
blocks: [
|
|
||||||
{
|
|
||||||
type: "section",
|
|
||||||
text: {
|
|
||||||
type: "mrkdwn",
|
|
||||||
text: `*${title}*`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "section",
|
|
||||||
text: {
|
|
||||||
type: "plain_text",
|
|
||||||
text: body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// add thumbnail if exists
|
|
||||||
if (thumbnail) {
|
|
||||||
data['blocks'].push({
|
|
||||||
type: "image",
|
|
||||||
image_url: thumbnail,
|
|
||||||
alt_text: "notification_thumbnail"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
data['blocks'].push(
|
|
||||||
{
|
|
||||||
type: "section",
|
|
||||||
text: {
|
|
||||||
type: "mrkdwn",
|
|
||||||
text: `<${url}|${url}>`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "context",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
type: "mrkdwn",
|
|
||||||
text: `*ID:* ${type}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
fetch(slack_webhook_url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendGenericNotification(data) {
|
function sendGenericNotification(data) {
|
||||||
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
|
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
|
||||||
logger.verbose(`Sending generic notification to ${webhook_url}`);
|
logger.verbose(`Sending generic notification to ${webhook_url}`);
|
||||||
|
|||||||
2642
backend/package-lock.json
generated
2642
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,44 @@
|
|||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "backend for YoutubeDL-Material",
|
"description": "backend for YoutubeDL-Material",
|
||||||
"main": "index.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"start": "pm2-runtime --raw pm2.config.js",
|
"start": "pm2-runtime --raw pm2.config.js",
|
||||||
"debug": "set YTDL_MODE=debug && node app.js"
|
"debug": "set YTDL_MODE=debug && node app.js",
|
||||||
|
"electron": "electron main.js",
|
||||||
|
"pack": "electron-builder --dir",
|
||||||
|
"build": "electron-builder"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "youtubedl.material",
|
||||||
|
"productName": "YoutubeDL-Material GUI",
|
||||||
|
"directories": {
|
||||||
|
"output": "dist"
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"category": "public.app-category.utilities",
|
||||||
|
"icon": "../src/assets/images/logo_512px.png",
|
||||||
|
"target": "dmg"
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": "nsis",
|
||||||
|
"icon": "../src/favicon.ico"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"!audio/*",
|
||||||
|
"!video/*",
|
||||||
|
"!users/*",
|
||||||
|
"!subscriptions/*",
|
||||||
|
"!appdata/*",
|
||||||
|
"*.js",
|
||||||
|
"authentication/auth.js",
|
||||||
|
"main.js",
|
||||||
|
"public/**/*",
|
||||||
|
"ffmpeg*",
|
||||||
|
"ffprobe*"
|
||||||
|
],
|
||||||
|
"asar": false
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -19,17 +52,17 @@
|
|||||||
},
|
},
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/builders": "^1.6.1",
|
"app-root-path": "^3.1.0",
|
||||||
"@discordjs/core": "^0.5.2",
|
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"async": "^3.2.3",
|
"async": "^3.2.3",
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.3.1",
|
||||||
"axios": "^0.21.2",
|
"axios": "^0.21.2",
|
||||||
"bcryptjs": "^2.4.0",
|
"bcryptjs": "^2.4.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"config": "^3.2.3",
|
"config": "^3.2.3",
|
||||||
"express": "^4.18.2",
|
"electron-log": "^4.4.8",
|
||||||
"express-session": "^1.17.3",
|
"electron-root-path": "^1.1.0",
|
||||||
|
"express": "^4.17.3",
|
||||||
"feed": "^4.2.2",
|
"feed": "^4.2.2",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^9.0.0",
|
||||||
@@ -57,9 +90,14 @@
|
|||||||
"rxjs": "^7.3.0",
|
"rxjs": "^7.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"unzipper": "^0.10.10",
|
"unzipper": "^0.10.10",
|
||||||
"uuidv4": "^6.2.13",
|
"uuidv4": "^6.0.6",
|
||||||
"winston": "^3.7.2",
|
"winston": "^3.7.2",
|
||||||
"xmlbuilder2": "^3.0.2",
|
"xmlbuilder2": "^3.0.2",
|
||||||
"youtube-dl": "^3.0.2"
|
"youtube-dl": "^3.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^24.1.2",
|
||||||
|
"electron-builder": "^23.6.0",
|
||||||
|
"electron-packager": "^17.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
backend/preload.js
Normal file
5
backend/preload.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const { contextBridge } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Expose the 'path' module to the renderer process
|
||||||
|
contextBridge.exposeInMainWorld('path', path);
|
||||||
@@ -92,10 +92,7 @@ async function getSubscriptionInfo(sub) {
|
|||||||
}
|
}
|
||||||
// if it's now valid, update
|
// if it's now valid, update
|
||||||
if (sub.name) {
|
if (sub.name) {
|
||||||
let sub_name = sub.name;
|
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub.name});
|
||||||
const sub_name_exists = await db_api.getRecord('subscriptions', {name: sub.name, isPlaylist: sub.isPlaylist, user_uid: sub.user_uid});
|
|
||||||
if (sub_name_exists) sub_name += ` - ${sub.id}`;
|
|
||||||
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub_name});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,20 +229,13 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
|
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
|
||||||
|
|
||||||
// get videos
|
// get videos
|
||||||
logger.verbose(`Subscription: getting list of videos to download for ${sub.name} with args: ${downloadConfig.join(',')}`);
|
logger.verbose(`Subscription: getting videos for subscription ${sub.name} with args: ${downloadConfig.join(',')}`);
|
||||||
|
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
|
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
|
||||||
// cleanup
|
// cleanup
|
||||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
||||||
|
|
||||||
// remove temporary archive file if it exists
|
|
||||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
|
||||||
const archive_exists = await fs.pathExists(archive_path);
|
|
||||||
if (archive_exists) {
|
|
||||||
await fs.unlink(archive_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||||
if (err && !output) {
|
if (err && !output) {
|
||||||
logger.error(err.stderr ? err.stderr : err.message);
|
logger.error(err.stderr ? err.stderr : err.message);
|
||||||
@@ -364,16 +354,6 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
|
|
||||||
downloadConfig.push(...qualityPath)
|
downloadConfig.push(...qualityPath)
|
||||||
|
|
||||||
// if archive is being used, we want to quickly skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub.custom_args) {
|
if (sub.custom_args) {
|
||||||
const customArgsArray = sub.custom_args.split(',,');
|
const customArgsArray = sub.custom_args.split(',,');
|
||||||
if (customArgsArray.indexOf('-f') !== -1) {
|
if (customArgsArray.indexOf('-f') !== -1) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable no-undef */
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
@@ -39,7 +38,6 @@ var db_api = require('../db');
|
|||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
const subscriptions_api = require('../subscriptions');
|
const subscriptions_api = require('../subscriptions');
|
||||||
const archive_api = require('../archive');
|
const archive_api = require('../archive');
|
||||||
const categories_api = require('../categories');
|
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
const NodeID3 = require('node-id3');
|
const NodeID3 = require('node-id3');
|
||||||
@@ -177,15 +175,6 @@ describe('Database', async function() {
|
|||||||
await db_api.removeRecord('test', {test_update: 'test'});
|
await db_api.removeRecord('test', {test_update: 'test'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Update records', async function() {
|
|
||||||
await db_api.insertRecordIntoTable('test', {test_update: 'test', key: 'test1'});
|
|
||||||
await db_api.insertRecordIntoTable('test', {test_update: 'test', key: 'test2'});
|
|
||||||
await db_api.updateRecords('test', {test_update: 'test'}, {added_field: true});
|
|
||||||
const updated_records = await db_api.getRecords('test', {added_field: true});
|
|
||||||
assert(updated_records.length === 2);
|
|
||||||
await db_api.removeRecord('test', {test_update: 'test'});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Remove property from record', async function() {
|
it('Remove property from record', async function() {
|
||||||
await db_api.insertRecordIntoTable('test', {test_keep: 'test', test_remove: 'test'});
|
await db_api.insertRecordIntoTable('test', {test_keep: 'test', test_remove: 'test'});
|
||||||
await db_api.removePropertyFromRecord('test', {test_keep: 'test'}, {test_remove: true});
|
await db_api.removePropertyFromRecord('test', {test_keep: 'test'}, {test_remove: true});
|
||||||
@@ -350,10 +339,8 @@ describe('Multi User', async function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Video player - normal', async function() {
|
describe('Video player - normal', async function() {
|
||||||
beforeEach(async function() {
|
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
|
||||||
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
|
await db_api.insertRecordIntoTable('files', sample_video_json);
|
||||||
await db_api.insertRecordIntoTable('files', sample_video_json);
|
|
||||||
});
|
|
||||||
const video_to_test = sample_video_json['uid'];
|
const video_to_test = sample_video_json['uid'];
|
||||||
it('Get video', async function() {
|
it('Get video', async function() {
|
||||||
const video_obj = await db_api.getVideo(video_to_test);
|
const video_obj = await db_api.getVideo(video_to_test);
|
||||||
@@ -510,23 +497,18 @@ describe('Downloader', function() {
|
|||||||
const new_args1 = ['--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
|
const new_args1 = ['--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
|
||||||
const updated_args1 = utils.injectArgs(original_args1, new_args1);
|
const updated_args1 = utils.injectArgs(original_args1, new_args1);
|
||||||
const expected_args1 = ['--no-resize-buffer', '--no-mtime', '--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
|
const expected_args1 = ['--no-resize-buffer', '--no-mtime', '--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
|
||||||
assert(JSON.stringify(updated_args1) === JSON.stringify(expected_args1));
|
assert(JSON.stringify(updated_args1), JSON.stringify(expected_args1));
|
||||||
|
|
||||||
const original_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3'];
|
const original_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3'];
|
||||||
const new_args2 = ['--add-metadata', '--embed-thumbnail', '--convert-thumbnails', 'jpg'];
|
const new_args2 = ['--add-metadata', '--embed-thumbnail', '--convert-thumbnails', 'jpg'];
|
||||||
const updated_args2 = utils.injectArgs(original_args2, new_args2);
|
const updated_args2 = utils.injectArgs(original_args2, new_args2);
|
||||||
const expected_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3', '--add-metadata', '--embed-thumbnail', '--convert-thumbnails', 'jpg'];
|
const expected_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3', '--add-metadata', '--embed-thumbnail', '--convert_thumbnails', 'jpg'];
|
||||||
assert(JSON.stringify(updated_args2) === JSON.stringify(expected_args2));
|
console.log(updated_args2);
|
||||||
|
assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2));
|
||||||
const original_args3 = ['-o', '%(title)s.%(ext)s'];
|
|
||||||
const new_args3 = ['--min-filesize','1'];
|
|
||||||
const updated_args3 = utils.injectArgs(original_args3, new_args3);
|
|
||||||
const expected_args3 = ['-o', '%(title)s.%(ext)s', '--min-filesize', '1'];
|
|
||||||
assert(JSON.stringify(updated_args3) === JSON.stringify(expected_args3));
|
|
||||||
});
|
});
|
||||||
describe('Twitch', async function () {
|
describe('Twitch', async function () {
|
||||||
const twitch_api = require('../twitch');
|
const twitch_api = require('../twitch');
|
||||||
const example_vod = '1710641401';
|
const example_vod = '1493770675';
|
||||||
it('Download VOD', async function() {
|
it('Download VOD', async function() {
|
||||||
const sample_path = path.join('test', 'sample.twitch_chat.json');
|
const sample_path = path.join('test', 'sample.twitch_chat.json');
|
||||||
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
||||||
@@ -608,7 +590,7 @@ describe('Tasks', function() {
|
|||||||
fs.copyFileSync('test/sample.mp4', 'video/sample.mp4');
|
fs.copyFileSync('test/sample.mp4', 'video/sample.mp4');
|
||||||
await tasks_api.executeTask('missing_db_records');
|
await tasks_api.executeTask('missing_db_records');
|
||||||
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
|
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
|
||||||
assert(!!imported_file === true);
|
assert(!!imported_file, true);
|
||||||
|
|
||||||
// post-test cleanup
|
// post-test cleanup
|
||||||
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
||||||
@@ -747,108 +729,3 @@ describe('Utils', async function() {
|
|||||||
assert(nested_obj2['test1'] && nested_obj2['test1']['test2'] && nested_obj2['test1']['test2']['test_sub']);
|
assert(nested_obj2['test1'] && nested_obj2['test1']['test2'] && nested_obj2['test1']['test2']['test_sub']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Categories', async function() {
|
|
||||||
beforeEach(async function() {
|
|
||||||
await db_api.connectToDB();
|
|
||||||
const new_category = {
|
|
||||||
name: 'test_category',
|
|
||||||
uid: uuid(),
|
|
||||||
rules: [],
|
|
||||||
custom_output: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
await db_api.insertRecordIntoTable('categories', new_category);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async function() {
|
|
||||||
await db_api.removeAllRecords('categories', {name: 'test_category'});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Categorize - includes', async function() {
|
|
||||||
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
|
||||||
preceding_operator: null,
|
|
||||||
comparator: 'includes',
|
|
||||||
property: 'title',
|
|
||||||
value: 'Sample'
|
|
||||||
});
|
|
||||||
|
|
||||||
const category = await categories_api.categorize([sample_video_json]);
|
|
||||||
assert(category && category.name === 'test_category');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Categorize - not includes', async function() {
|
|
||||||
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
|
||||||
preceding_operator: null,
|
|
||||||
comparator: 'not_includes',
|
|
||||||
property: 'title',
|
|
||||||
value: 'Sample'
|
|
||||||
});
|
|
||||||
|
|
||||||
const category = await categories_api.categorize([sample_video_json]);
|
|
||||||
assert(!category);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Categorize - equals', async function() {
|
|
||||||
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
|
||||||
preceding_operator: null,
|
|
||||||
comparator: 'equals',
|
|
||||||
property: 'uploader',
|
|
||||||
value: 'Sample Uploader'
|
|
||||||
});
|
|
||||||
|
|
||||||
const category = await categories_api.categorize([sample_video_json]);
|
|
||||||
console.log(category);
|
|
||||||
assert(category && category.name === 'test_category');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Categorize - not equals', async function() {
|
|
||||||
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
|
||||||
preceding_operator: null,
|
|
||||||
comparator: 'not_equals',
|
|
||||||
property: 'uploader',
|
|
||||||
value: 'Sample Uploader'
|
|
||||||
});
|
|
||||||
|
|
||||||
const category = await categories_api.categorize([sample_video_json]);
|
|
||||||
assert(!category);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Categorize - AND', async function() {
|
|
||||||
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
|
||||||
preceding_operator: null,
|
|
||||||
comparator: 'equals',
|
|
||||||
property: 'uploader',
|
|
||||||
value: 'Sample Uploader'
|
|
||||||
});
|
|
||||||
|
|
||||||
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
|
||||||
preceding_operator: 'and',
|
|
||||||
comparator: 'not_includes',
|
|
||||||
property: 'title',
|
|
||||||
value: 'Sample'
|
|
||||||
});
|
|
||||||
|
|
||||||
const category = await categories_api.categorize([sample_video_json]);
|
|
||||||
assert(!category);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Categorize - OR', async function() {
|
|
||||||
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
|
||||||
preceding_operator: null,
|
|
||||||
comparator: 'equals',
|
|
||||||
property: 'uploader',
|
|
||||||
value: 'Sample Uploader'
|
|
||||||
});
|
|
||||||
|
|
||||||
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
|
||||||
preceding_operator: 'or',
|
|
||||||
comparator: 'not_includes',
|
|
||||||
property: 'title',
|
|
||||||
value: 'Sample'
|
|
||||||
});
|
|
||||||
|
|
||||||
const category = await categories_api.categorize([sample_video_json]);
|
|
||||||
assert(category);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -4,28 +4,19 @@ const logger = require('./logger');
|
|||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { promisify } = require('util');
|
|
||||||
const child_process = require('child_process');
|
|
||||||
|
|
||||||
async function getCommentsForVOD(vodId) {
|
async function getCommentsForVOD(clientID, clientSecret, vodId) {
|
||||||
|
const { promisify } = require('util');
|
||||||
|
const child_process = require('child_process');
|
||||||
const exec = promisify(child_process.exec);
|
const exec = promisify(child_process.exec);
|
||||||
|
|
||||||
// Reject invalid params to prevent command injection attack
|
// Reject invalid params to prevent command injection attack
|
||||||
if (!vodId.match(/^[0-9a-z]+$/)) {
|
if (!clientID.match(/^[0-9a-z]+$/) || !clientSecret.match(/^[0-9a-z]+$/) || !vodId.match(/^[0-9a-z]+$/)) {
|
||||||
logger.error('VOD ID must be purely alphanumeric. Twitch chat download failed!');
|
logger.error('Client ID, client secret, and VOD ID must be purely alphanumeric. Twitch chat download failed!');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const is_windows = process.platform === 'win32';
|
const result = await exec(`tcd --video ${vodId} --client-id ${clientID} --client-secret ${clientSecret} --format json -o appdata`, {stdio:[0,1,2]});
|
||||||
const cliExt = is_windows ? '.exe' : ''
|
|
||||||
const cliPath = `TwitchDownloaderCLI${cliExt}`
|
|
||||||
|
|
||||||
if (!fs.existsSync(cliPath)) {
|
|
||||||
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await exec(`TwitchDownloaderCLI chatdownload -u ${vodId} -o appdata/${vodId}.json`, {stdio:[0,1,2]});
|
|
||||||
|
|
||||||
if (result['stderr']) {
|
if (result['stderr']) {
|
||||||
logger.error(`Failed to download twitch comments for ${vodId}`);
|
logger.error(`Failed to download twitch comments for ${vodId}`);
|
||||||
@@ -82,7 +73,9 @@ async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
|
|||||||
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
|
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
|
||||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
const chat = await getCommentsForVOD(vodId);
|
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
|
||||||
|
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
|
||||||
|
const chat = await getCommentsForVOD(twitch_client_id, twitch_client_secret, vodId);
|
||||||
|
|
||||||
// save file if needed params are included
|
// save file if needed params are included
|
||||||
let file_path = null;
|
let file_path = null;
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ exports.createZipFile = async (zip_file_path, file_paths) => {
|
|||||||
await archive.finalize();
|
await archive.finalize();
|
||||||
|
|
||||||
// wait a tiny bit for the zip to reload in fs
|
// wait a tiny bit for the zip to reload in fs
|
||||||
await exports.wait(100);
|
await wait(100);
|
||||||
return zip_file_path;
|
return zip_file_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,11 +414,10 @@ exports.injectArgs = (original_args, new_args) => {
|
|||||||
if (CONSTS.YTDL_ARGS_WITH_VALUES.has(new_arg)) {
|
if (CONSTS.YTDL_ARGS_WITH_VALUES.has(new_arg)) {
|
||||||
if (original_args.includes(new_arg)) {
|
if (original_args.includes(new_arg)) {
|
||||||
const original_index = original_args.indexOf(new_arg);
|
const original_index = original_args.indexOf(new_arg);
|
||||||
updated_args.splice(original_index, 2);
|
original_args.splice(original_index, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
updated_args.push(new_arg, new_args[i + 1]);
|
updated_args.push(new_arg, new_args[i + 1]);
|
||||||
i++; // we need to skip the arg value on the next loop
|
|
||||||
} else {
|
} else {
|
||||||
if (!original_args.includes(new_arg)) {
|
if (!original_args.includes(new_arg)) {
|
||||||
updated_args.push(new_arg);
|
updated_args.push(new_arg);
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import platform
|
|
||||||
import requests
|
|
||||||
import shutil
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from github import Github
|
|
||||||
|
|
||||||
machine = platform.machine()
|
|
||||||
|
|
||||||
def isARM():
|
|
||||||
return True if machine.startswith('arm') else False
|
|
||||||
|
|
||||||
def getLatestFileInRepo(repo, search_string):
|
|
||||||
# Create an unauthenticated instance of the Github object
|
|
||||||
g = Github(os.environ.get('GH_TOKEN'))
|
|
||||||
|
|
||||||
# Replace with the repository owner and name
|
|
||||||
repo = g.get_repo(repo)
|
|
||||||
|
|
||||||
# Get all releases of the repository
|
|
||||||
releases = repo.get_releases()
|
|
||||||
|
|
||||||
# Loop through the releases in reverse order (from latest to oldest)
|
|
||||||
for release in list(releases):
|
|
||||||
# Get the release assets (files attached to the release)
|
|
||||||
assets = release.get_assets()
|
|
||||||
|
|
||||||
# Loop through the assets
|
|
||||||
for asset in assets:
|
|
||||||
if re.search(search_string, asset.name):
|
|
||||||
print(f'Downloading: {asset.name}')
|
|
||||||
response = requests.get(asset.browser_download_url)
|
|
||||||
with open(asset.name, 'wb') as f:
|
|
||||||
f.write(response.content)
|
|
||||||
print(f'Download complete: {asset.name}. Unzipping...')
|
|
||||||
shutil.unpack_archive(asset.name, './')
|
|
||||||
print(f'Unzipping complete!')
|
|
||||||
os.remove(asset.name)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# If no matching release is found, print a message
|
|
||||||
print(f'No release found with {search_string}')
|
|
||||||
|
|
||||||
def getLatestCLIRelease():
|
|
||||||
isArm = isARM()
|
|
||||||
searchString = r'.*CLI.*' + "LinuxArm.zip" if isArm else "Linux-x64.zip"
|
|
||||||
getLatestFileInRepo("lay295/TwitchDownloader", searchString)
|
|
||||||
|
|
||||||
getLatestCLIRelease()
|
|
||||||
41
main.js
41
main.js
@@ -1,41 +0,0 @@
|
|||||||
const { app, BrowserWindow } = require('electron');
|
|
||||||
const path = require('path');
|
|
||||||
const url = require('url');
|
|
||||||
|
|
||||||
let win;
|
|
||||||
|
|
||||||
function createWindow() {
|
|
||||||
win = new BrowserWindow({ width: 800, height: 600 });
|
|
||||||
|
|
||||||
// load the dist folder from Angular
|
|
||||||
win.loadURL(
|
|
||||||
url.format({
|
|
||||||
pathname: path.join(__dirname, `/dist/index.html`),
|
|
||||||
protocol: 'file:',
|
|
||||||
slashes: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// The following is optional and will open the DevTools:
|
|
||||||
// win.webContents.openDevTools()
|
|
||||||
|
|
||||||
win.on('closed', () => {
|
|
||||||
win = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('ready', createWindow);
|
|
||||||
|
|
||||||
// on macOS, closing the window doesn't quit the app
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// initialize the app's main window
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (win === null) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
1074
package-lock.json
generated
1074
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -39,7 +39,7 @@
|
|||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"filesize": "^10.0.7",
|
"filesize": "^6.1.0",
|
||||||
"fingerprintjs2": "^2.1.0",
|
"fingerprintjs2": "^2.1.0",
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"material-icons": "^1.10.8",
|
"material-icons": "^1.10.8",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"ngx-avatars": "^1.4.1",
|
"ngx-avatars": "^1.4.1",
|
||||||
"ngx-file-drop": "^13.0.0",
|
"ngx-file-drop": "^13.0.0",
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^6.6.3",
|
||||||
"rxjs-compat": "^6.6.7",
|
"rxjs-compat": "^6.0.0-rc.0",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "~4.8.4",
|
"typescript": "~4.8.4",
|
||||||
"xliff-to-json": "^1.0.4",
|
"xliff-to-json": "^1.0.4",
|
||||||
@@ -60,21 +60,20 @@
|
|||||||
"@angular/language-service": "^15.0.1",
|
"@angular/language-service": "^15.0.1",
|
||||||
"@types/core-js": "^2.5.2",
|
"@types/core-js": "^2.5.2",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/jasmine": "^4.3.1",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||||
"@typescript-eslint/parser": "^4.29.0",
|
"@typescript-eslint/parser": "^4.29.0",
|
||||||
"ajv": "^7.2.4",
|
"ajv": "^7.2.4",
|
||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"electron": "^19.1.9",
|
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"jasmine-core": "~3.6.0",
|
"jasmine-core": "~3.6.0",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
"karma": "~6.4.2",
|
"karma": "~6.3.16",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-cli": "~1.0.1",
|
"karma-cli": "~1.0.1",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~4.0.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"openapi-typescript-codegen": "^0.23.0",
|
"openapi-typescript-codegen": "^0.23.0",
|
||||||
"protractor": "~7.0.0",
|
"protractor": "~7.0.0",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep .ngx-file-drop__content {
|
::ng-deep .ngx-file-drop__content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: -12px;
|
top: -12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''" [loading]="false"></app-unified-file-card>
|
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -76,9 +76,8 @@ export class RecentVideosComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(public postsService: PostsService, private router: Router) {
|
constructor(public postsService: PostsService, private router: Router) {
|
||||||
// get cached file count
|
// get cached file count
|
||||||
const sub_id_appendix = this.sub_id ? `_${this.sub_id}` : ''
|
if (localStorage.getItem('cached_file_count')) {
|
||||||
if (localStorage.getItem(`cached_file_count${sub_id_appendix}`)) {
|
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
|
||||||
this.cached_file_count = +localStorage.getItem(`cached_file_count${sub_id_appendix}`) <= 10 ? +localStorage.getItem(`cached_file_count${sub_id_appendix}`) : 10;
|
|
||||||
this.loading_files = Array(this.cached_file_count).fill(0);
|
this.loading_files = Array(this.cached_file_count).fill(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<ng-container i18n="Update binary to">Update binary to:</ng-container> {{element.data}}
|
<ng-container i18n="Update binary to">Update binary to:</ng-container> {{element.data}}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="element.key == 'delete_old_files'">
|
<ng-container *ngIf="element.key == 'delete_old_files'">
|
||||||
<ng-container i18n="Delete old files">Delete old files:</ng-container> {{element.data.files_to_remove.length}}
|
<ng-container i18n="Delete old files">Delete old files:</ng-container> {{element.data.uids.length}}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ mat-header-cell, mat-cell {
|
|||||||
border-radius: 16px 16px 16px 16px !important;
|
border-radius: 16px 16px 16px 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep mat-row {
|
::ng-deep mat-row {
|
||||||
height: fit-content !important;
|
height: fit-content !important;
|
||||||
}
|
}
|
||||||
@@ -171,6 +171,6 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep.mat-mdc-menu-panel {
|
::ng-deep.mat-mdc-menu-panel {
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
}
|
}
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
top: -12px;
|
top: -12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep.mat-menu-panel {
|
::ng-deep.mat-menu-panel {
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep.mdc-list-item__primary-text {
|
::ng-deep.mdc-list-item__primary-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -31,11 +31,9 @@
|
|||||||
<mat-form-field class="value-input">
|
<mat-form-field class="value-input">
|
||||||
<input matInput [(ngModel)]="rule['value']">
|
<input matInput [(ngModel)]="rule['value']">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<span class="rule-buttons">
|
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
|
||||||
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
|
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
|
||||||
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
|
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
|
||||||
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
|
|
||||||
</span>
|
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.operator-select {
|
.operator-select {
|
||||||
width: 90px;
|
width: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-select {
|
.property-select {
|
||||||
@@ -14,16 +14,3 @@
|
|||||||
.value-input {
|
.value-input {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep.mdc-list-item {
|
|
||||||
height: 75px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host ::ng-deep.mdc-list-item__content {
|
|
||||||
pointer-events: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-buttons {
|
|
||||||
position: relative;
|
|
||||||
top: 8px;
|
|
||||||
}
|
|
||||||
@@ -37,8 +37,7 @@
|
|||||||
<input [(ngModel)]="new_file.thumbnailURL" matInput [disabled]="!editing || new_file.thumbnailPath">
|
<input [(ngModel)]="new_file.thumbnailURL" matInput [disabled]="!editing || new_file.thumbnailPath">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field *ngIf="initialized && postsService.categories" class="info-field">
|
<mat-form-field *ngIf="initialized && postsService.categories" class="info-field">
|
||||||
<mat-label i18n="Category">Category</mat-label>
|
<mat-select placeholder="Category" i18n-placeholder="Category" [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
|
||||||
<mat-select [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
|
|
||||||
<mat-option [value]="{}">
|
<mat-option [value]="{}">
|
||||||
N/A
|
N/A
|
||||||
</mat-option>
|
</mat-option>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit, Inject } from '@angular/core';
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
import { filesize } from 'filesize';
|
import filesize from 'filesize';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { Category, DatabaseFile } from 'api-types';
|
import { Category, DatabaseFile } from 'api-types';
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="db_file || playlist[currentIndex]"></ng-container>
|
<ng-container *ngIf="db_file || playlist[currentIndex]"></ng-container>
|
||||||
<button (click)="openFileInfoDialog()" *ngIf="db_file || db_playlist" mat-icon-button><mat-icon>info</mat-icon></button>
|
<button (click)="openFileInfoDialog()" *ngIf="db_file || db_playlist" mat-icon-button><mat-icon>info</mat-icon></button>
|
||||||
<button *ngIf="db_file && db_file.url.includes('twitch.tv')" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
|
<button *ngIf="db_file && db_file.url.includes('twitch.tv') && postsService['config']['API']['use_twitch_API']" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
<app-concurrent-stream *ngIf="db_file && api && postsService.config" (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
|
<app-concurrent-stream *ngIf="db_file && api && postsService.config" (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
|
||||||
|
|
||||||
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists']">
|
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists'] && postsService['config']['API']['use_twitch_API']">
|
||||||
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv')">
|
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv')">
|
||||||
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
|
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -269,9 +269,25 @@
|
|||||||
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
|
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-1">
|
<div class="col-12 mt-3">
|
||||||
|
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_twitch_API']"><ng-container i18n="Use Twitch API setting">Use Twitch API</ng-container></mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="new_config['API']['use_twitch_API']" class="col-12 mt-1">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<mat-form-field class="text-field" color="accent">
|
||||||
|
<mat-label i18n="Twitch Client ID">Twitch Client ID</mat-label>
|
||||||
|
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_ID']" matInput required>
|
||||||
|
<mat-hint><a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch Client ID setting hint">Generating an ID/secret is easy!</ng-container></a></mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-2">
|
||||||
|
<mat-form-field class="text-field" color="accent">
|
||||||
|
<mat-label i18n="Twitch Client Secret">Twitch Client Secret</mat-label>
|
||||||
|
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_secret']" matInput required>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<div class="col-12 mt-2">
|
<div class="col-12 mt-2">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,20 +401,6 @@
|
|||||||
<mat-hint>Place endpoint URL here to integrate with services like Zapier and Automatisch.</mat-hint>
|
<mat-hint>Place endpoint URL here to integrate with services like Zapier and Automatisch.</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mb-2 mt-3">
|
|
||||||
<mat-form-field class="text-field" color="accent">
|
|
||||||
<mat-label i18n="Discord Webhook URL">Discord Webhook URL</mat-label>
|
|
||||||
<input placeholder="https://discord.com/api/webhooks/<webhook_id>/<webhook_token>" [(ngModel)]="new_config['API']['discord_webhook_URL']" matInput>
|
|
||||||
<mat-hint><a target="_blank" href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"><ng-container i18n="Discord API setting hint">See docs here.</ng-container></a></mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 mb-2 mt-3">
|
|
||||||
<mat-form-field class="text-field" color="accent">
|
|
||||||
<mat-label i18n="Slack Webhook URL">Slack Webhook URL</mat-label>
|
|
||||||
<input placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" [(ngModel)]="new_config['API']['slack_webhook_URL']" matInput>
|
|
||||||
<mat-hint><a target="_blank" href="https://api.slack.com/messaging/webhooks"><ng-container i18n="Slack API setting hint">See docs here.</ng-container></a></mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-checkbox color="accent" [disabled]="!new_config['Extra']['enable_notifications']" [(ngModel)]="new_config['API']['use_ntfy_API']"><ng-container i18n="Use ntfy API setting">Use ntfy API</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [disabled]="!new_config['Extra']['enable_notifications']" [(ngModel)]="new_config['API']['use_ntfy_API']"><ng-container i18n="Use ntfy API setting">Use ntfy API</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep .mat-mdc-tab-body {
|
::ng-deep .mat-mdc-tab-body {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
latestGithubRelease = null;
|
latestGithubRelease = null;
|
||||||
CURRENT_VERSION = CURRENT_VERSION
|
CURRENT_VERSION = CURRENT_VERSION
|
||||||
|
|
||||||
tabs = ['main', 'downloader', 'extra', 'database', 'notifications', 'advanced', 'users', 'logs'];
|
tabs = ['main', 'downloader', 'extra', 'database', 'advanced', 'users', 'logs'];
|
||||||
tabIndex = 0;
|
tabIndex = 0;
|
||||||
|
|
||||||
INDEX_TO_TAB = Object.assign({}, this.tabs);
|
INDEX_TO_TAB = Object.assign({}, this.tabs);
|
||||||
|
|||||||
@@ -4184,612 +4184,6 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">Restore button</note>
|
<note priority="1" from="description">Restore button</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html">
|
|
||||||
<source>Archives</source>
|
|
||||||
<target state="translated">Archive</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/app.component.html</context>
|
|
||||||
<context context-type="linenumber">26</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Archives menu label</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5ca707824ab93066c7d9b44e1b8bf216725c2c22" datatype="html">
|
|
||||||
<source>Filter</source>
|
|
||||||
<target state="translated">Filter</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
|
|
||||||
<context context-type="linenumber">3</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Filter</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="51a161ce175abcd44f6c6cbd0e996681bf553ac3" datatype="html">
|
|
||||||
<source>Delete selected</source>
|
|
||||||
<target state="translated">Ausgewählte löschen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
|
|
||||||
<context context-type="linenumber">77</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Delete selected</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="c41475a25c9f9d9639db9efa56637603a77528b4" datatype="html">
|
|
||||||
<source>Download archive</source>
|
|
||||||
<target state="translated">Archiv herunterladen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
|
|
||||||
<context context-type="linenumber">80</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Download archive</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
|
|
||||||
<source>None</source>
|
|
||||||
<target state="translated">Kein</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
|
|
||||||
<context context-type="linenumber">84</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
|
|
||||||
<context context-type="linenumber">126</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
|
|
||||||
<context context-type="linenumber">27</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
|
|
||||||
<context context-type="linenumber">36</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">None</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="4b3972c3e9485218508a95f7a4ce7758e3f09ced" datatype="html">
|
|
||||||
<source>Upload</source>
|
|
||||||
<target state="translated">Hochladen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
|
|
||||||
<context context-type="linenumber">137</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">30</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Upload</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6549265851868599441" datatype="html">
|
|
||||||
<source>Video</source>
|
|
||||||
<target state="translated">Video</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
|
|
||||||
<context context-type="linenumber">40</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="347407180135731058" datatype="html">
|
|
||||||
<source>Audio</source>
|
|
||||||
<target state="translated">Audio</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
|
|
||||||
<context context-type="linenumber">44</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8953483585652369683" datatype="html">
|
|
||||||
<source>Archive successfully imported!</source>
|
|
||||||
<target state="translated">Archiv erfolgreich importiert!</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
|
|
||||||
<context context-type="linenumber">130</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="3159807825117518005" datatype="html">
|
|
||||||
<source>Delete archives</source>
|
|
||||||
<target state="translated">Archive löschen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
|
|
||||||
<context context-type="linenumber">152</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="7022070615528435141" datatype="html">
|
|
||||||
<source>Delete</source>
|
|
||||||
<target state="translated">Löschen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
|
|
||||||
<context context-type="linenumber">154</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
|
||||||
<context context-type="linenumber">175</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="019d4bd6a5690f0cfa0ecf346a4e6bf7f0d8debb" datatype="html">
|
|
||||||
<source>Remove</source>
|
|
||||||
<target state="translated">Entfernen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.html</context>
|
|
||||||
<context context-type="linenumber">23</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Remove</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6219551536751479443" datatype="html">
|
|
||||||
<source>Finished downloading</source>
|
|
||||||
<target state="translated">Herunterladen abgeschlossen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
|
||||||
<context context-type="linenumber">17</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5947241266456580665" datatype="html">
|
|
||||||
<source>Download failed</source>
|
|
||||||
<target state="translated">Herunterladen fehlgeschlagen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
|
||||||
<context context-type="linenumber">18</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8443034725057696949" datatype="html">
|
|
||||||
<source>Task finished</source>
|
|
||||||
<target state="translated">Aufgabe abgeschlossen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
|
||||||
<context context-type="linenumber">19</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8564202903947049539" datatype="html">
|
|
||||||
<source>Play</source>
|
|
||||||
<target state="translated">Wiedergabe</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
|
||||||
<context context-type="linenumber">30</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5a105e7bd7e7db6ea211fe950fc9f317379acceb" datatype="html">
|
|
||||||
<source>No notifications available</source>
|
|
||||||
<target state="translated">Keine Benachrichtigungen verfügbar</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications/notifications.component.html</context>
|
|
||||||
<context context-type="linenumber">1</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">No notifications available</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5709555629190115111" datatype="html">
|
|
||||||
<source>View task</source>
|
|
||||||
<target state="translated">Aufgabe ansehen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
|
||||||
<context context-type="linenumber">33</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6876310993601590130" datatype="html">
|
|
||||||
<source>Download completed</source>
|
|
||||||
<target state="translated">Herunterladen abgeschlossen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
|
|
||||||
<context context-type="linenumber">23</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="4578192247039196794" datatype="html">
|
|
||||||
<source>Task</source>
|
|
||||||
<target state="translated">Aufgabe</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
|
|
||||||
<context context-type="linenumber">31</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="7911845622864460134" datatype="html">
|
|
||||||
<source>Video only</source>
|
|
||||||
<target state="translated">Nur Video</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
|
|
||||||
<context context-type="linenumber">55</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6437411876967154040" datatype="html">
|
|
||||||
<source>Audio only</source>
|
|
||||||
<target state="translated">Nur Audio</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
|
|
||||||
<context context-type="linenumber">60</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6268070779441507380" datatype="html">
|
|
||||||
<source>Download Date</source>
|
|
||||||
<target state="translated">Herunterladedatum</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
|
|
||||||
<context context-type="linenumber">13</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cdf5297d8d080a78e8b10debc5c38b7845a3cbe7" datatype="html">
|
|
||||||
<source>Do not ask for confirmation</source>
|
|
||||||
<target state="translated">Nicht nach einer Bestätigung fragen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
|
|
||||||
<context context-type="linenumber">19</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Do not ask for confirmation</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="7b4585a9072f3c1292972c14a3d0e14978fbfc9c" datatype="html">
|
|
||||||
<source>Delete old files:</source>
|
|
||||||
<target state="translated">Alte Dateien löschen:</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/tasks/tasks.component.html</context>
|
|
||||||
<context context-type="linenumber">66</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Delete old files</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="9176960997786930103" datatype="html">
|
|
||||||
<source>Error for: <x id="PH" equiv-text="task['title']"/></source>
|
|
||||||
<target state="translated">Fehler für: <x id="PH" equiv-text="task['title']"/></target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/tasks/tasks.component.ts</context>
|
|
||||||
<context context-type="linenumber">174</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="11a0771f88158a540a54e0e4ec5d25733d65fc0e" datatype="html">
|
|
||||||
<source>Favorite</source>
|
|
||||||
<target state="translated">Favorit</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
|
|
||||||
<context context-type="linenumber">26</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Favorite button</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5ac5a0e5ffe8e5623b40696f4c2403c17349271f" datatype="html">
|
|
||||||
<source>Sidepanel mode</source>
|
|
||||||
<target state="translated">Seitenleisten-Modus</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/dialogs/about-dialog/about-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">42</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Sidepanel mode</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1a9b415816364f554ee411020e65219092655271" datatype="html">
|
|
||||||
<source>Title filter</source>
|
|
||||||
<target state="translated">Titelfilter</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
|
|
||||||
<context context-type="linenumber">8</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Title filter</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="9aa62bf1a535a97a4d752bbc5cf1c31af0f0c1f7" datatype="html">
|
|
||||||
<source>Supports regex</source>
|
|
||||||
<target state="translated">Unterstützt reguläre Ausdrücke</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
|
|
||||||
<context context-type="linenumber">10</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Supports regex</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
|
|
||||||
<source>User</source>
|
|
||||||
<target state="translated">Benutzer</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
|
|
||||||
<context context-type="linenumber">25</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">User</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="d57c023a4cf63b2f12c10328c15b636ff18929aa" datatype="html">
|
|
||||||
<source>Best</source>
|
|
||||||
<target state="translated">Beste</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/main/main.component.html</context>
|
|
||||||
<context context-type="linenumber">24,25</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Best</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="35cf4cdcedc8ef3f94b6100e0d86836e31dbb908" datatype="html">
|
|
||||||
<source>Force autoplay</source>
|
|
||||||
<target state="translated">Automatische Wiedergabe erzwingen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">235</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Force autoplay setting</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8bcabdf6b16cad0313a86c7e940c5e3ad7f9f8ab" datatype="html">
|
|
||||||
<source>Notifications</source>
|
|
||||||
<target state="translated">Benachrichtigungen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">376</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Notifications settings label</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="c40370dc182b5e4333828b70f7478bde58bb5cfe" datatype="html">
|
|
||||||
<source>Enable notifications</source>
|
|
||||||
<target state="translated">Benachrichtigungen aktivieren</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">382</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Enable notifications setting</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="33a7c6d5ff3515fa237f1fd4e43df8b65373954d" datatype="html">
|
|
||||||
<source>Enable all notifications</source>
|
|
||||||
<target state="translated">Alle Benachrichtigungen aktivieren</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">385</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Enable all notifications setting</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="9c562d26e041390ecc3f49dabc51cc50ebba7469" datatype="html">
|
|
||||||
<source>Allowed notification types</source>
|
|
||||||
<target state="translated">Erlaubte Benachrichtigungsarten</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">389</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Allowed notification types</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="c5dc5fbcce45e9b1530e2a5c2baa8ebe722aef4c" datatype="html">
|
|
||||||
<source>Download complete</source>
|
|
||||||
<target state="translated">Herunterladen abgeschlossen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">391</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Download complete</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2361a4f76caaa4574803fbcdca8b0a47c91cc7ed" datatype="html">
|
|
||||||
<source>Task finished</source>
|
|
||||||
<target state="translated">Aufgabe abgeschlossen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">393</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Task finished</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="38992954440d6afb54aeb58af12ca0123ee5e26e" datatype="html">
|
|
||||||
<source>Use Telegram API</source>
|
|
||||||
<target state="translated">Telegram-API verwenden</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">432</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Use Telegram API setting</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="eeb0ba2e4743901d8f5eebd9a3529aa1f236c608" datatype="html">
|
|
||||||
<source>Create bot here.</source>
|
|
||||||
<target state="translated">Bot hier erstellen.</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">438</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Telegram bot create link</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="a4ed8eba1e057e67d5c2d87b52230f182b3dae4e" datatype="html">
|
|
||||||
<source>Restart required.</source>
|
|
||||||
<target state="translated">Neustart erforderlich.</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">465</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Restart required hint</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6785427850041119037" datatype="html">
|
|
||||||
<source>Delete category</source>
|
|
||||||
<target state="translated">Kategorie löschen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
|
||||||
<context context-type="linenumber">173</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="7332320960988475089" datatype="html">
|
|
||||||
<source>Successfully deleted <x id="category name" equiv-text="category['name']"/>!</source>
|
|
||||||
<target state="translated"><x id="category name" equiv-text="category['name']"/> erfolgreich gelöscht!</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
|
||||||
<context context-type="linenumber">183</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8c6e24eab969d9f63a8a0e9d617aee3b99e28ae6" datatype="html">
|
|
||||||
<source>Play all</source>
|
|
||||||
<target state="translated">Alle wiedergeben</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
|
|
||||||
<context context-type="linenumber">17</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Play all</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="674a999dd48d7da565ffdd105602261b8a4761ea" datatype="html">
|
|
||||||
<source>Download zip</source>
|
|
||||||
<target state="translated">ZIP herunterladen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
|
|
||||||
<context context-type="linenumber">18</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Download zip</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="0ed98b4c6ec1db6708a963e8a2699478ac97f55c" datatype="html">
|
|
||||||
<source>Add subscription</source>
|
|
||||||
<target state="translated">Abonnement hinzufügen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/subscriptions/subscriptions.component.html</context>
|
|
||||||
<context context-type="linenumber">60</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Add subscription</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="3533826530554274875" datatype="html">
|
|
||||||
<source>Upload Date</source>
|
|
||||||
<target state="translated">Hochladedatum</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
|
|
||||||
<context context-type="linenumber">17</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8953033926734869941" datatype="html">
|
|
||||||
<source>Name</source>
|
|
||||||
<target state="translated">Name</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
|
|
||||||
<context context-type="linenumber">21</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2492098975665776610" datatype="html">
|
|
||||||
<source>File Size</source>
|
|
||||||
<target state="translated">Dateigröße</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
|
|
||||||
<context context-type="linenumber">25</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="ea2b65121b93921fe54692025da9b9e3ce779ad5" datatype="html">
|
|
||||||
<source>Task settings - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></source>
|
|
||||||
<target state="translated">Aufgabeneinstellungen - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
|
|
||||||
<context context-type="linenumber">1</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Task settings</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="7410432243549869948" datatype="html">
|
|
||||||
<source>Duration</source>
|
|
||||||
<target state="translated">Dauer</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
|
|
||||||
<context context-type="linenumber">29</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="c65dd978b3c7566551c0ebefb234c2d41942b847" datatype="html">
|
|
||||||
<source>Delete files older than</source>
|
|
||||||
<target state="translated">Löschen von Dateien, die älter sind als</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
|
|
||||||
<context context-type="linenumber">6</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Delete files older than</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
|
|
||||||
<source>ID</source>
|
|
||||||
<target state="translated">Kennung</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
|
|
||||||
<context context-type="linenumber">47</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">ID</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="c150a30bbbdb175b4d08820196a9acb66b167653" datatype="html">
|
|
||||||
<source>Archives empty</source>
|
|
||||||
<target state="translated">Archive leer</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
|
|
||||||
<context context-type="linenumber">72</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Archives empty</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8425787787095143143" datatype="html">
|
|
||||||
<source>Would you like to delete <x id="selected archives amount" equiv-text="this.selection.selected.length"/> archive(s)?</source>
|
|
||||||
<target state="translated">Möchten Sie <x id="selected archives amount" equiv-text="this.selection.selected.length"/> Archiv(e) löschen?</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
|
|
||||||
<context context-type="linenumber">153</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2525880134753073592" datatype="html">
|
|
||||||
<source>Successfully deleted archive items!</source>
|
|
||||||
<target state="translated">Archivelemente erfolgreich gelöscht!</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
|
|
||||||
<context context-type="linenumber">172</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8224301330941792118" datatype="html">
|
|
||||||
<source>Failed to delete archive items!</source>
|
|
||||||
<target state="translated">Fehler beim Löschen von Archivelementen!</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
|
|
||||||
<context context-type="linenumber">174</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8643601595923420698" datatype="html">
|
|
||||||
<source>Retry download</source>
|
|
||||||
<target state="translated">Herunterladen erneut versuchen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
|
||||||
<context context-type="linenumber">31</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8571838164752006148" datatype="html">
|
|
||||||
<source>View error</source>
|
|
||||||
<target state="translated">Fehler ansehen</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
|
|
||||||
<context context-type="linenumber">32</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1879058637439215882" datatype="html">
|
|
||||||
<source>Download error</source>
|
|
||||||
<target state="translated">Herunterladefehler</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
|
|
||||||
<context context-type="linenumber">27</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5000203534763292992" datatype="html">
|
|
||||||
<source>Download restarted!</source>
|
|
||||||
<target state="translated">Herunterladen neu gestartet!</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
|
|
||||||
<context context-type="linenumber">72</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1f2809e6a99d511fdb6eaf041d785fe54d0680cc" datatype="html">
|
|
||||||
<source>File card size</source>
|
|
||||||
<target state="translated">Dateikartengröße</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/dialogs/about-dialog/about-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">54</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">File card size</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="c35ef0f03a863d33b04aae6807f140397a50f491" datatype="html">
|
|
||||||
<source>Generate RSS URL</source>
|
|
||||||
<target state="translated">RSS-URL generieren</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
|
|
||||||
<context context-type="linenumber">1</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">306</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Generate RSS URL</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1091872159779006651" datatype="html">
|
|
||||||
<source>You must input a time!</source>
|
|
||||||
<target state="translated">Sie müssen eine Zeit eingeben!</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.ts</context>
|
|
||||||
<context context-type="linenumber">70</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="fd467148a18f0921c10d116d4e0174fe29452be4" datatype="html">
|
|
||||||
<source>See documentation here.</source>
|
|
||||||
<target state="translated">Siehe Dokumentation hier.</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">307</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">RSS feed documentation</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="61d6b5fa4311b1c617b66dad72496f9dd43b07b4" datatype="html">
|
|
||||||
<source>Be careful enabling this with multi-user mode! User data may be exposed.</source>
|
|
||||||
<target state="translated">Seien Sie vorsichtig, wenn Sie diese Funktion im Mehrbenutzermodus aktivieren! Benutzerdaten können offengelegt werden.</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">305</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">RSS Feed prefix</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html">
|
|
||||||
<source>Download error</source>
|
|
||||||
<target state="translated">Herunterladefehler</target>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
|
||||||
<context context-type="linenumber">392</context>
|
|
||||||
</context-group>
|
|
||||||
<note priority="1" from="description">Download error</note>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
BIN
src/assets/images/logo_512px.png
Normal file
BIN
src/assets/images/logo_512px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
28
src/assets/splash.html
Normal file
28
src/assets/splash.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Splash Screen</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #222;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="center">
|
||||||
|
<img src="images/logo_128px.png" alt="Logo">
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user