mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-08 04:20:08 +03:00
Compare commits
13 Commits
gh-pages
...
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/appdata/cookies.txt
|
||||
backend/public
|
||||
backend/dist
|
||||
src/assets/i18n/*.json
|
||||
|
||||
# User Files
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -2,7 +2,7 @@
|
||||
FROM ubuntu:22.04 AS ffmpeg
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
# Use script due local build compability
|
||||
COPY docker-utils/ffmpeg-fetch.sh .
|
||||
COPY ffmpeg-fetch.sh .
|
||||
RUN chmod +x ffmpeg-fetch.sh
|
||||
RUN sh ./ffmpeg-fetch.sh
|
||||
|
||||
@@ -47,15 +47,6 @@ RUN npm config set strict-ssl false && \
|
||||
npm install --prod && \
|
||||
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
|
||||
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 clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN pip install pycryptodomex
|
||||
RUN pip install tdh-tcd pycryptodomex
|
||||
WORKDIR /app
|
||||
# User 1000 already exist from base image
|
||||
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 fs = require('fs-extra');
|
||||
const { promisify } = require('util');
|
||||
@@ -56,48 +60,7 @@ let debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
const admin_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
||||
|
||||
// logging setup
|
||||
|
||||
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();
|
||||
// required initialization
|
||||
|
||||
// config values
|
||||
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'};
|
||||
}
|
||||
|
||||
// don't overwrite config if it already happened.. NOT
|
||||
// let alreadyWritten = db.get('configWriteFlag').value();
|
||||
exports.initialize = () => {
|
||||
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
|
||||
config_api.configExistsCheck();
|
||||
// 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();
|
||||
|
||||
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 }));
|
||||
app.use(bodyParser.json());
|
||||
// checks if config exists, if not, a config is auto generated
|
||||
config_api.configExistsCheck();
|
||||
|
||||
// use passport
|
||||
app.use(auth_api.passport.initialize());
|
||||
app.use(auth_api.passport.session());
|
||||
setAndLoadConfig();
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
// use passport
|
||||
app.use(auth_api.passport.initialize());
|
||||
}
|
||||
|
||||
exports.initialize();
|
||||
|
||||
// actual functions
|
||||
|
||||
@@ -252,7 +256,7 @@ async function simplifyDBFileStructure() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function startServer() {
|
||||
exports.startServer = async () => {
|
||||
if (process.env.USING_HEROKU && process.env.PORT) {
|
||||
// default to heroku port if using heroku
|
||||
backendPort = process.env.PORT || backendPort;
|
||||
@@ -545,7 +549,9 @@ async function loadConfig() {
|
||||
}
|
||||
|
||||
// start the server here
|
||||
startServer();
|
||||
if (typeof require !== 'undefined' && require.main === module) {
|
||||
exports.startServer();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1639,7 +1645,9 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
|
||||
else file_path = null;
|
||||
}
|
||||
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 fileSize = stat.size;
|
||||
|
||||
@@ -23,12 +23,7 @@
|
||||
"download_only_mode": false,
|
||||
"allow_autoplay": true,
|
||||
"enable_downloads_manager": true,
|
||||
"allow_playlist_categorization": true,
|
||||
"force_autoplay": false,
|
||||
"enable_notifications": true,
|
||||
"enable_all_notifications": true,
|
||||
"allowed_notification_types": [],
|
||||
"enable_rss_feed": false
|
||||
"allow_playlist_categorization": true
|
||||
},
|
||||
"API": {
|
||||
"use_API_key": false,
|
||||
@@ -40,17 +35,7 @@
|
||||
"twitch_client_secret": "",
|
||||
"twitch_auto_download_chat": false,
|
||||
"use_sponsorblock_API": 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": ""
|
||||
"generate_NFO_files": false
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
|
||||
@@ -208,6 +208,9 @@ const DEFAULT_CONFIG = {
|
||||
"API_key": "",
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": "",
|
||||
"use_twitch_API": false,
|
||||
"twitch_client_ID": "",
|
||||
"twitch_client_secret": "",
|
||||
"twitch_auto_download_chat": false,
|
||||
"use_sponsorblock_API": false,
|
||||
"generate_NFO_files": false,
|
||||
@@ -219,8 +222,7 @@ const DEFAULT_CONFIG = {
|
||||
"use_telegram_API": false,
|
||||
"telegram_bot_token": "",
|
||||
"telegram_chat_id": "",
|
||||
"webhook_URL": "",
|
||||
"discord_webhook_URL": ""
|
||||
"webhook_URL": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
|
||||
@@ -110,6 +110,18 @@ exports.CONFIG_ITEMS = {
|
||||
'key': 'ytdl_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': {
|
||||
'key': 'ytdl_twitch_auto_download_chat',
|
||||
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
||||
@@ -158,10 +170,6 @@ exports.CONFIG_ITEMS = {
|
||||
'key': 'ytdl_webhook_url',
|
||||
'path': 'YoutubeDLMaterial.API.webhook_URL'
|
||||
},
|
||||
'ytdl_discord_webhook_url': {
|
||||
'key': 'ytdl_discord_webhook_url',
|
||||
'path': 'YoutubeDLMaterial.API.discord_webhook_URL'
|
||||
},
|
||||
|
||||
|
||||
// Themes
|
||||
@@ -346,6 +354,4 @@ const YTDL_ARGS_WITH_VALUES = [
|
||||
// we're using a Set here for performance
|
||||
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';
|
||||
|
||||
@@ -720,14 +720,7 @@ exports.updateRecord = async (table, filter_obj, update_obj, nested_mode = false
|
||||
exports.updateRecords = async (table, filter_obj, update_obj) => {
|
||||
// local db override
|
||||
if (using_local_db) {
|
||||
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').each((record) => {
|
||||
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();
|
||||
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ exports.clearDownload = async (download_uid) => {
|
||||
|
||||
async function handleDownloadError(download, error_message, error_type = null) {
|
||||
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});
|
||||
}
|
||||
|
||||
@@ -245,10 +245,11 @@ async function collectInfo(download_uid) {
|
||||
options.customOutput = category['custom_output'];
|
||||
options.noRelativePath = true;
|
||||
args = await exports.generateArgs(url, type, options, download['user_uid']);
|
||||
args = utils.filterArgs(args, ['--no-simulate']);
|
||||
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
|
||||
|
||||
@@ -271,7 +272,6 @@ async function collectInfo(download_uid) {
|
||||
files_to_check_for_progress: files_to_check_for_progress,
|
||||
expected_file_size: expected_file_size,
|
||||
title: playlist_title ? playlist_title : info['title'],
|
||||
category: stripped_category,
|
||||
prefetched_info: null
|
||||
});
|
||||
}
|
||||
@@ -314,7 +314,7 @@ async function downloadQueuedFile(download_uid) {
|
||||
clearInterval(download_checker);
|
||||
if (err) {
|
||||
logger.error(err.stderr);
|
||||
await handleDownloadError(download, err.stderr, 'unknown_error');
|
||||
await handleDownloadError(download, err.stderr);
|
||||
resolve(false);
|
||||
return;
|
||||
} 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);
|
||||
|
||||
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];
|
||||
vodId = vodId.split('?')[0];
|
||||
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) => {
|
||||
return new Promise(resolve => {
|
||||
// remove bad args
|
||||
const temp_args = utils.filterArgs(args, ['--no-simulate']);
|
||||
const new_args = [...temp_args];
|
||||
const new_args = [...args];
|
||||
|
||||
const archiveArgIndex = new_args.indexOf('--download-archive');
|
||||
if (archiveArgIndex !== -1) {
|
||||
@@ -596,7 +595,7 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
||||
logger.error(error_message);
|
||||
if (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);
|
||||
}
|
||||
|
||||
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 logger = require('./logger');
|
||||
const utils = require('./utils');
|
||||
const consts = require('./consts');
|
||||
|
||||
const { uuid } = require('uuidv4');
|
||||
|
||||
const fetch = require('node-fetch');
|
||||
const { gotify } = require("gotify");
|
||||
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 = {
|
||||
task_finished: 'Task finished',
|
||||
@@ -22,7 +18,7 @@ const NOTIFICATION_TYPE_TO_TITLE = {
|
||||
const NOTIFICATION_TYPE_TO_BODY = {
|
||||
task_finished: (notification) => notification['data']['task_title'],
|
||||
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 = {
|
||||
@@ -61,9 +57,6 @@ exports.sendNotification = async (notification) => {
|
||||
if (config_api.getConfigItem('ytdl_webhook_url')) {
|
||||
sendGenericNotification(data);
|
||||
}
|
||||
if (config_api.getConfigItem('ytdl_discord_webhook_url')) {
|
||||
sendDiscordNotification(data);
|
||||
}
|
||||
|
||||
await db_api.insertRecordIntoTable('notifications', notification);
|
||||
return notification;
|
||||
@@ -86,9 +79,9 @@ exports.sendDownloadNotification = async (file, user_uid) => {
|
||||
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;
|
||||
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);
|
||||
return await exports.sendNotification(notification);
|
||||
}
|
||||
@@ -151,29 +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'});
|
||||
}
|
||||
|
||||
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 sendGenericNotification(data) {
|
||||
const webhook_url = config_api.getConfigItem('ytdl_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",
|
||||
"version": "1.0.0",
|
||||
"description": "backend for YoutubeDL-Material",
|
||||
"main": "index.js",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"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": {
|
||||
"type": "git",
|
||||
@@ -19,17 +52,17 @@
|
||||
},
|
||||
"homepage": "",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.6.1",
|
||||
"@discordjs/core": "^0.5.2",
|
||||
"app-root-path": "^3.1.0",
|
||||
"archiver": "^5.3.1",
|
||||
"async": "^3.2.3",
|
||||
"async-mutex": "^0.4.0",
|
||||
"async-mutex": "^0.3.1",
|
||||
"axios": "^0.21.2",
|
||||
"bcryptjs": "^2.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"config": "^3.2.3",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-root-path": "^1.1.0",
|
||||
"express": "^4.17.3",
|
||||
"feed": "^4.2.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
@@ -57,9 +90,14 @@
|
||||
"rxjs": "^7.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"unzipper": "^0.10.10",
|
||||
"uuidv4": "^6.2.13",
|
||||
"uuidv4": "^6.0.6",
|
||||
"winston": "^3.7.2",
|
||||
"xmlbuilder2": "^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 (sub.name) {
|
||||
let sub_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});
|
||||
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);
|
||||
|
||||
// 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 => {
|
||||
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
|
||||
// cleanup
|
||||
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);
|
||||
if (err && !output) {
|
||||
logger.error(err.stderr ? err.stderr : err.message);
|
||||
@@ -364,16 +354,6 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
|
||||
downloadConfig.push(...qualityPath)
|
||||
|
||||
// if archive is being used, we want to quickly skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
||||
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useYoutubeDLArchive) {
|
||||
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
||||
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
|
||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||
await fs.writeFile(archive_path, archive_text);
|
||||
downloadConfig.push('--download-archive', archive_path);
|
||||
}
|
||||
|
||||
if (sub.custom_args) {
|
||||
const customArgsArray = sub.custom_args.split(',,');
|
||||
if (customArgsArray.indexOf('-f') !== -1) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-undef */
|
||||
const assert = require('assert');
|
||||
const low = require('lowdb')
|
||||
const winston = require('winston');
|
||||
@@ -39,7 +38,6 @@ var db_api = require('../db');
|
||||
const utils = require('../utils');
|
||||
const subscriptions_api = require('../subscriptions');
|
||||
const archive_api = require('../archive');
|
||||
const categories_api = require('../categories');
|
||||
const fs = require('fs-extra');
|
||||
const { uuid } = require('uuidv4');
|
||||
const NodeID3 = require('node-id3');
|
||||
@@ -177,15 +175,6 @@ describe('Database', async function() {
|
||||
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() {
|
||||
await db_api.insertRecordIntoTable('test', {test_keep: 'test', test_remove: 'test'});
|
||||
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() {
|
||||
beforeEach(async function() {
|
||||
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
|
||||
await db_api.insertRecordIntoTable('files', sample_video_json);
|
||||
});
|
||||
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
|
||||
await db_api.insertRecordIntoTable('files', sample_video_json);
|
||||
const video_to_test = sample_video_json['uid'];
|
||||
it('Get video', async function() {
|
||||
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 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'];
|
||||
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 new_args2 = ['--add-metadata', '--embed-thumbnail', '--convert-thumbnails', 'jpg'];
|
||||
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'];
|
||||
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));
|
||||
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'];
|
||||
console.log(updated_args2);
|
||||
assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2));
|
||||
});
|
||||
describe('Twitch', async function () {
|
||||
const twitch_api = require('../twitch');
|
||||
const example_vod = '1710641401';
|
||||
const example_vod = '1493770675';
|
||||
it('Download VOD', async function() {
|
||||
const sample_path = path.join('test', 'sample.twitch_chat.json');
|
||||
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
||||
@@ -608,7 +590,7 @@ describe('Tasks', function() {
|
||||
fs.copyFileSync('test/sample.mp4', 'video/sample.mp4');
|
||||
await tasks_api.executeTask('missing_db_records');
|
||||
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
|
||||
assert(!!imported_file === true);
|
||||
assert(!!imported_file, true);
|
||||
|
||||
// post-test cleanup
|
||||
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
||||
@@ -746,109 +728,4 @@ describe('Utils', async function() {
|
||||
assert(nested_obj2['test1'] && nested_obj2['test1']['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 fs = require('fs-extra')
|
||||
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);
|
||||
|
||||
// Reject invalid params to prevent command injection attack
|
||||
if (!vodId.match(/^[0-9a-z]+$/)) {
|
||||
logger.error('VOD ID must be purely alphanumeric. Twitch chat download failed!');
|
||||
if (!clientID.match(/^[0-9a-z]+$/) || !clientSecret.match(/^[0-9a-z]+$/) || !vodId.match(/^[0-9a-z]+$/)) {
|
||||
logger.error('Client ID, client secret, and VOD ID must be purely alphanumeric. Twitch chat download failed!');
|
||||
return null;
|
||||
}
|
||||
|
||||
const is_windows = process.platform === 'win32';
|
||||
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]});
|
||||
const result = await exec(`tcd --video ${vodId} --client-id ${clientID} --client-secret ${clientSecret} --format json -o appdata`, {stdio:[0,1,2]});
|
||||
|
||||
if (result['stderr']) {
|
||||
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) {
|
||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_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
|
||||
let file_path = null;
|
||||
|
||||
@@ -92,7 +92,7 @@ exports.createZipFile = async (zip_file_path, file_paths) => {
|
||||
await archive.finalize();
|
||||
|
||||
// wait a tiny bit for the zip to reload in fs
|
||||
await exports.wait(100);
|
||||
await wait(100);
|
||||
return zip_file_path;
|
||||
}
|
||||
|
||||
@@ -414,11 +414,10 @@ exports.injectArgs = (original_args, new_args) => {
|
||||
if (CONSTS.YTDL_ARGS_WITH_VALUES.has(new_arg)) {
|
||||
if (original_args.includes(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]);
|
||||
i++; // we need to skip the arg value on the next loop
|
||||
} else {
|
||||
if (!original_args.includes(new_arg)) {
|
||||
updated_args.push(new_arg);
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Copyright 2018 Isaac Grynsztein
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,3 +0,0 @@
|
||||
The official YoutubeDL-Material chart
|
||||
|
||||
Repo: https://github.com/Tzahi12345/YoutubeDL-Material
|
||||
@@ -1,4 +0,0 @@
|
||||
# Artifact Hub repository metadata file
|
||||
repositoryID: ff79ae03-57c1-4f18-8368-5e085d06e2f1
|
||||
owners:
|
||||
- name: tzahi12345
|
||||
@@ -1,22 +0,0 @@
|
||||
apiVersion: v1
|
||||
entries:
|
||||
youtubedl-material:
|
||||
- apiVersion: v1
|
||||
created: "2021-01-01T05:51:36.304331-05:00"
|
||||
description: A Material Design frontend for youtube-dl
|
||||
digest: f7340d24fb051ade30b890db3b5c483a884b8459316dcf2ffc6fb9413d41252e
|
||||
home: https://github.com/Tzahi12345/YoutubeDL-Material/
|
||||
icon: https://i.imgur.com/IKOlr0N.png
|
||||
keywords:
|
||||
- youtubedl-material
|
||||
- youtube-dl
|
||||
maintainers:
|
||||
- email: IsaacMGrynsztein@gmail.com
|
||||
name: tzahi12345
|
||||
name: youtubedl-material
|
||||
sources:
|
||||
- https://github.com/Tzahi12345/YoutubeDL-Material/
|
||||
urls:
|
||||
- youtubedl-material-0.0.1.tgz
|
||||
version: 0.0.1
|
||||
generated: "2021-01-01T05:51:36.3003569-05:00"
|
||||
@@ -1,14 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: ytdl-material-claim-appdata
|
||||
name: ytdl-material-claim-appdata
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ default "1Gi" .Values.appdataClaimSize }}
|
||||
status: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: ytdl-material-claim-audio
|
||||
name: ytdl-material-claim-audio
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ default "50Gi" .Values.audioClaimSize }}
|
||||
status: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: ytdl-material-claim-subscriptions
|
||||
name: ytdl-material-claim-subscriptions
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ default "50Gi" .Values.subscriptionsClaimSize }}
|
||||
status: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: ytdl-material-claim-users
|
||||
name: ytdl-material-claim-users
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ default "50Gi" .Values.usersClaimSize }}
|
||||
status: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: ytdl-material-claim-video
|
||||
name: ytdl-material-claim-video
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ default "50Gi" .Values.videoClaimSize }}
|
||||
status: {}
|
||||
Binary file not shown.
@@ -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",
|
||||
"crypto-js": "^4.1.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"filesize": "^10.0.7",
|
||||
"filesize": "^6.1.0",
|
||||
"fingerprintjs2": "^2.1.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"material-icons": "^1.10.8",
|
||||
@@ -47,7 +47,7 @@
|
||||
"ngx-avatars": "^1.4.1",
|
||||
"ngx-file-drop": "^13.0.0",
|
||||
"rxjs": "^6.6.3",
|
||||
"rxjs-compat": "^6.6.7",
|
||||
"rxjs-compat": "^6.0.0-rc.0",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "~4.8.4",
|
||||
"xliff-to-json": "^1.0.4",
|
||||
@@ -60,21 +60,20 @@
|
||||
"@angular/language-service": "^15.0.1",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "^4.3.1",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"ajv": "^7.2.4",
|
||||
"codelyzer": "^6.0.0",
|
||||
"electron": "^19.1.9",
|
||||
"eslint": "^7.32.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~6.4.2",
|
||||
"karma": "~6.3.16",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-cli": "~1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"openapi-typescript-codegen": "^0.23.0",
|
||||
"protractor": "~7.0.0",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
:host ::ng-deep .ngx-file-drop__content {
|
||||
::ng-deep .ngx-file-drop__content {
|
||||
width: 100%;
|
||||
top: -12px;
|
||||
position: relative;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="container">
|
||||
<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' : '' ]">
|
||||
<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>
|
||||
|
||||
@@ -76,9 +76,8 @@ export class RecentVideosComponent implements OnInit {
|
||||
|
||||
constructor(public postsService: PostsService, private router: Router) {
|
||||
// get cached file count
|
||||
const sub_id_appendix = this.sub_id ? `_${this.sub_id}` : ''
|
||||
if (localStorage.getItem(`cached_file_count${sub_id_appendix}`)) {
|
||||
this.cached_file_count = +localStorage.getItem(`cached_file_count${sub_id_appendix}`) <= 10 ? +localStorage.getItem(`cached_file_count${sub_id_appendix}`) : 10;
|
||||
if (localStorage.getItem('cached_file_count')) {
|
||||
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
|
||||
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>
|
||||
<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>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
@@ -31,6 +31,6 @@ mat-header-cell, mat-cell {
|
||||
border-radius: 16px 16px 16px 16px !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep mat-row {
|
||||
::ng-deep mat-row {
|
||||
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;
|
||||
}
|
||||
@@ -8,10 +8,10 @@
|
||||
top: -12px;
|
||||
}
|
||||
|
||||
:host ::ng-deep.mat-menu-panel {
|
||||
::ng-deep.mat-menu-panel {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep.mdc-list-item__primary-text {
|
||||
::ng-deep.mdc-list-item__primary-text {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -31,11 +31,9 @@
|
||||
<mat-form-field class="value-input">
|
||||
<input matInput [(ngModel)]="rule['value']">
|
||||
</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 === 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>
|
||||
</span>
|
||||
<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 (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.operator-select {
|
||||
width: 90px;
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.property-select {
|
||||
@@ -13,17 +13,4 @@
|
||||
|
||||
.value-input {
|
||||
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">
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="initialized && postsService.categories" class="info-field">
|
||||
<mat-label i18n="Category">Category</mat-label>
|
||||
<mat-select [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
|
||||
<mat-select placeholder="Category" i18n-placeholder="Category" [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
|
||||
<mat-option [value]="{}">
|
||||
N/A
|
||||
</mat-option>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { filesize } from 'filesize';
|
||||
import filesize from 'filesize';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Category, DatabaseFile } from 'api-types';
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</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 *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>
|
||||
@@ -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>
|
||||
|
||||
<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')">
|
||||
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
|
||||
</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-form-field>
|
||||
</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>
|
||||
</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">
|
||||
<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>
|
||||
@@ -385,13 +401,6 @@
|
||||
<mat-hint>Place endpoint URL here to integrate with services like Zapier and Automatisch.</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="Discord webhook URL">Discord Webhook URL</mat-label>
|
||||
<input placeholder="https://discord.com/api/webhooks/<webhook_id>/<webhook_token>" [(ngModel)]="new_config['API']['discord_webhook_URL']" matInput>
|
||||
<mat-hint><a target="_blank" href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"><ng-container i18n="Gotify API setting hint">See docs here.</ng-container></a></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-mdc-tab-body {
|
||||
::ng-deep .mat-mdc-tab-body {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export class SettingsComponent implements OnInit {
|
||||
latestGithubRelease = null;
|
||||
CURRENT_VERSION = CURRENT_VERSION
|
||||
|
||||
tabs = ['main', 'downloader', 'extra', 'database', 'notifications', 'advanced', 'users', 'logs'];
|
||||
tabs = ['main', 'downloader', 'extra', 'database', 'advanced', 'users', 'logs'];
|
||||
tabIndex = 0;
|
||||
|
||||
INDEX_TO_TAB = Object.assign({}, this.tabs);
|
||||
|
||||
@@ -4184,612 +4184,6 @@
|
||||
</context-group>
|
||||
<note priority="1" from="description">Restore button</note>
|
||||
</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>
|
||||
</file>
|
||||
</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