Merge remote-tracking branch 'origin/master'

pull/226/head
Hosted Weblate 4 years ago
commit b5b9e84950
No known key found for this signature in database
GPG Key ID: A3FAAA06E6569B4C

@ -25,6 +25,23 @@ jobs:
cd backend
npm install
sudo npm install -g @angular/cli
- name: prepare localization
run: |
sudo npm install -g xliff-to-json
xliff-to-json ./src/assets/i18n
- name: Set hash
id: vars
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: create-json
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "version.json"
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/'
- name: build
run: ng build --prod
- name: prepare artifact upload

@ -13,6 +13,23 @@ jobs:
steps:
- name: checkout code
uses: actions/checkout@v2
- name: prepare localization
run: |
sudo npm install -g xliff-to-json
xliff-to-json ./src/assets/i18n
- name: Set hash
id: vars
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: create-json
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "version.json"
json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/'
- name: setup platform emulator
uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build

@ -10,6 +10,23 @@ jobs:
steps:
- name: checkout code
uses: actions/checkout@v2
- name: prepare localization
run: |
sudo npm install -g xliff-to-json
xliff-to-json ./src/assets/i18n
- name: Set hash
id: vars
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: create-json
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "version.json"
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/'
- name: setup platform emulator
uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build

@ -1,4 +1,4 @@
FROM alpine:3.12 as frontend
FROM alpine:latest as frontend
RUN apk add --no-cache \
npm
@ -15,7 +15,7 @@ RUN ng build --prod
#--------------#
FROM alpine:3.12
FROM alpine:latest
ENV UID=1000 \
GID=1000 \

@ -77,6 +77,10 @@ Alternatively, you can port forward the port specified in the config (defaults t
## Docker
### Host-specific instructions
If you're on a Synology NAS, unRAID or any other possible special case you can check if there's known issues or instructions both in the issue tracker and in the [Wiki!](https://github.com/Tzahi12345/YoutubeDL-Material/wiki#environment-specific-guideshelp)
### Setup
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.

@ -142,6 +142,14 @@ var validDownloadingAgents = [
const subscription_timeouts = {};
let version_info = null;
if (fs.existsSync('version.json')) {
version_info = fs.readJSONSync('version.json');
logger.verbose(`Version info: ${JSON.stringify(version_info, null, 2)}`);
} else {
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();
let writeConfigMode = process.env.write_ytdl_config;
@ -837,7 +845,7 @@ async function checkExistsWithTimeout(filePath, timeout) {
fs.access(filePath, fs.constants.R_OK, function (err) {
if (!err) {
clearTimeout(timer);
watcher.close();
if (watcher) watcher.close();
resolve();
}
});
@ -847,7 +855,7 @@ async function checkExistsWithTimeout(filePath, timeout) {
var watcher = fs.watch(dir, function (eventType, filename) {
if (eventType === 'rename' && filename === basename) {
clearTimeout(timer);
watcher.close();
if (watcher) watcher.close();
resolve();
}
});
@ -932,6 +940,10 @@ app.post('/api/setConfig', optionalJwt, function(req, res) {
}
});
app.get('/api/versionInfo', (req, res) => {
res.send({version_info: version_info});
});
app.post('/api/restartServer', optionalJwt, (req, res) => {
// delayed by a little bit so that the client gets a response
setTimeout(() => {restartServer()}, 100);
@ -975,8 +987,9 @@ app.post('/api/downloadFile', optionalJwt, async function(req, res) {
const url = req.body.url;
const type = req.body.type;
const user_uid = req.isAuthenticated() ? req.user.uid : null;
var options = {
const options = {
customArgs: req.body.customArgs,
additionalArgs: req.body.additionalArgs,
customOutput: req.body.customOutput,
selectedHeight: req.body.selectedHeight,
customQualityConfiguration: req.body.customQualityConfiguration,
@ -984,7 +997,7 @@ app.post('/api/downloadFile', optionalJwt, async function(req, res) {
youtubePassword: req.body.youtubePassword,
ui_uid: req.body.ui_uid,
cropFileSettings: req.body.cropFileSettings
}
};
const download = await downloader_api.createDownload(url, type, options, user_uid);
@ -1000,6 +1013,26 @@ app.post('/api/killAllDownloads', optionalJwt, async function(req, res) {
res.send(result_obj);
});
app.post('/api/generateArgs', optionalJwt, async function(req, res) {
const url = req.body.url;
const type = req.body.type;
const user_uid = req.isAuthenticated() ? req.user.uid : null;
const options = {
customArgs: req.body.customArgs,
additionalArgs: req.body.additionalArgs,
customOutput: req.body.customOutput,
selectedHeight: req.body.selectedHeight,
customQualityConfiguration: req.body.customQualityConfiguration,
youtubeUsername: req.body.youtubeUsername,
youtubePassword: req.body.youtubePassword,
ui_uid: req.body.ui_uid,
cropFileSettings: req.body.cropFileSettings
};
const args = await downloader_api.generateArgs(url, type, options, user_uid, true);
res.send({args: args});
});
// gets all download mp3s
app.get('/api/getMp3s', optionalJwt, async function(req, res) {
// TODO: simplify

@ -33,7 +33,8 @@
"use_twitch_API": false,
"twitch_API_key": "",
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false
"use_sponsorblock_API": false,
"generate_NFO_files": false
},
"Themes": {
"default_theme": "default",

@ -208,7 +208,8 @@ const DEFAULT_CONFIG = {
"use_twitch_API": false,
"twitch_API_key": "",
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false
"use_sponsorblock_API": false,
"generate_NFO_files": false
},
"Themes": {
"default_theme": "default",

@ -114,6 +114,11 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_use_sponsorblock_api',
'path': 'YoutubeDLMaterial.API.use_sponsorblock_API'
},
'ytdl_generate_nfo_files': {
'key': 'ytdl_generate_nfo_files',
'path': 'YoutubeDLMaterial.API.generate_NFO_files'
},
// Themes
'ytdl_default_theme': {

@ -217,8 +217,7 @@ function generateFileObject(file_path, type) {
var title = jsonobj.title;
var url = jsonobj.webpage_url;
var uploader = jsonobj.uploader;
var upload_date = jsonobj.upload_date;
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
var upload_date = utils.formatDateString(jsonobj.upload_date);
var size = stats.size;

@ -11,6 +11,7 @@ const youtubedl = require('youtube-dl');
const logger = require('./logger');
const config_api = require('./config');
const twitch_api = require('./twitch');
const { create } = require('xmlbuilder2');
const categories_api = require('./categories');
const utils = require('./utils');
@ -190,7 +191,7 @@ async function collectInfo(download_uid) {
options.customFileFolderPath = user_path + path.sep;
}
let args = await generateArgs(url, type, options, download['user_uid']);
let args = await exports.generateArgs(url, type, options, download['user_uid']);
// get video info prior to download
let info = await getVideoInfoByURL(url, args, download_uid);
@ -209,7 +210,7 @@ async function collectInfo(download_uid) {
if (category && category['custom_output']) {
options.customOutput = category['custom_output'];
options.noRelativePath = true;
args = await generateArgs(url, type, options, download['user_uid']);
args = await exports.generateArgs(url, type, options, download['user_uid']);
info = await getVideoInfoByURL(url, args, download_uid);
}
@ -328,6 +329,10 @@ async function downloadQueuedFile(download_uid) {
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
}
if (config_api.getConfigItem('ytdl_generate_nfo_files')) {
exports.generateNFOFile(output_json, `${filepath_no_extension}.nfo`);
}
if (options.cropFileSettings) {
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
}
@ -369,7 +374,7 @@ async function downloadQueuedFile(download_uid) {
// helper functions
async function generateArgs(url, type, options, user_uid = null) {
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
@ -510,7 +515,7 @@ async function generateArgs(url, type, options, user_uid = null) {
// filter out incompatible args
downloadConfig = filterArgs(downloadConfig, is_audio);
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
if (!simulated) logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
return downloadConfig;
}
@ -604,3 +609,18 @@ async function checkDownloadPercent(download_uid) {
await db_api.updateRecord('download_queue', {uid: download_uid}, {percent_complete: percent_complete});
});
}
exports.generateNFOFile = (info, output_path) => {
const nfo_obj = {
episodedetails: {
title: info['fulltitle'],
episode: info['playlist_index'] ? info['playlist_index'] : undefined,
premiered: utils.formatDateString(info['upload_date']),
plot: `${info['uploader_url']}\n${info['description']}\n${info['playlist_title'] ? info['playlist_title'] : ''}`,
director: info['artist'] ? info['artist'] : info['uploader']
}
};
const doc = create(nfo_obj);
const xml = doc.end({ prettyPrint: true });
fs.writeFileSync(output_path, xml);
}

@ -4,6 +4,38 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@oozcitak/dom": {
"version": "1.15.10",
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
"integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
"requires": {
"@oozcitak/infra": "1.0.8",
"@oozcitak/url": "1.0.4",
"@oozcitak/util": "8.3.8"
}
},
"@oozcitak/infra": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
"integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
"requires": {
"@oozcitak/util": "8.3.8"
}
},
"@oozcitak/url": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
"integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
"requires": {
"@oozcitak/infra": "1.0.8",
"@oozcitak/util": "8.3.8"
}
},
"@oozcitak/util": {
"version": "8.3.8",
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
},
"@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@ -316,11 +348,11 @@
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.2.tgz",
"integrity": "sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==",
"requires": {
"follow-redirects": "^1.10.0"
"follow-redirects": "^1.14.0"
}
},
"backoff": {
@ -1117,6 +1149,11 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -1267,9 +1304,9 @@
}
},
"follow-redirects": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
},
"forever-agent": {
"version": "0.6.1",
@ -3030,6 +3067,11 @@
"memory-pager": "^1.0.2"
}
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
@ -3593,6 +3635,37 @@
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
"integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q=="
},
"xmlbuilder2": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.0.2.tgz",
"integrity": "sha512-h4MUawGY21CTdhV4xm3DG9dgsqyhDkZvVJBx88beqX8wJs3VgyGQgAn5VreHuae6unTQxh115aMK5InCVmOIKw==",
"requires": {
"@oozcitak/dom": "1.15.10",
"@oozcitak/infra": "1.0.8",
"@oozcitak/util": "8.3.8",
"@types/node": "*",
"js-yaml": "3.14.0"
},
"dependencies": {
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
},
"js-yaml": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
}
}
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

@ -33,7 +33,7 @@
"archiver": "^3.1.1",
"async": "^3.1.0",
"async-mutex": "^0.3.1",
"axios": "^0.21.1",
"axios": "^0.21.2",
"bcryptjs": "^2.4.0",
"compression": "^1.7.4",
"config": "^3.2.3",
@ -66,6 +66,7 @@
"unzipper": "^0.10.10",
"uuidv4": "^6.0.6",
"winston": "^3.2.1",
"xmlbuilder2": "^3.0.2",
"youtube-dl": "^3.0.2"
}
}

@ -329,7 +329,8 @@ function generateOptionsForSubscriptionDownload(sub, user_uid) {
selectedHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null,
customFileFolderPath: getAppendedBasePath(sub, basePath),
customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`,
customArchivePath: path.join(__dirname, basePath, 'archives', sub.name)
customArchivePath: path.join(__dirname, basePath, 'archives', sub.name),
additionalArgs: sub.custom_args
}
return base_download_options;
@ -542,5 +543,6 @@ module.exports = {
deleteSubscriptionFile : deleteSubscriptionFile,
getVideosForSub : getVideosForSub,
initialize : initialize,
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
}

File diff suppressed because one or more lines are too long

@ -293,6 +293,7 @@ describe('Downloader', function() {
const downloader_api = require('../downloader');
downloader_api.initialize(db_api);
const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
const options = {
ui_uid: uuid(),
user: 'admin'
@ -325,4 +326,27 @@ describe('Downloader', function() {
it('Pause file', async function() {
});
it('Generate args', async function() {
const args = await downloader_api.generateArgs(url, 'video', options);
console.log(args);
});
it('Generate args - subscription', async function() {
subscriptions_api.initialize(db_api, logger);
const sub = await subscriptions_api.getSubscription(sub_id);
const sub_options = subscriptions_api.generateOptionsForSubscriptionDownload(sub, 'admin');
const args = await downloader_api.generateArgs(url, 'video', sub_options, 'admin');
console.log(args);
});
it('Generate kodi NFO file', async function() {
const nfo_file_path = './test/sample.nfo';
if (fs.existsSync(nfo_file_path)) {
fs.unlinkSync(nfo_file_path);
}
const sample_json = fs.readJSONSync('./test/sample.info.json');
downloader_api.generateNFOFile(sample_json, nfo_file_path);
assert(fs.existsSync(nfo_file_path), true);
});
});

@ -45,8 +45,7 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
files.push(jsonobj);
continue;
}
var upload_date = jsonobj.upload_date;
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : null;
var upload_date = formatDateString(jsonobj.upload_date);
var isaudio = type === 'audio';
var file_obj = new File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
@ -295,6 +294,10 @@ function removeFileExtension(filename) {
return filename_parts.join('.');
}
function formatDateString(date_string) {
return date_string ? `${date_string.substring(0, 4)}-${date_string.substring(4, 6)}-${date_string.substring(6, 8)}` : 'N/A';
}
function createEdgeNGrams(str) {
if (str && str.length > 3) {
const minGram = 3
@ -389,6 +392,7 @@ module.exports = {
getCurrentDownloader: getCurrentDownloader,
recFindByExt: recFindByExt,
removeFileExtension: removeFileExtension,
formatDateString: formatDateString,
cropFile: cropFile,
createEdgeNGrams: createEdgeNGrams,
wait: wait,

@ -15,7 +15,7 @@ services:
- ./users:/app/users
ports:
- "8998:17442"
image: tzahi12345/youtubedl-material:latest
image: tzahi12345/youtubedl-material:nightly
ytdl-mongo-db:
image: mongo
ports:

165
package-lock.json generated

@ -1622,9 +1622,9 @@
}
},
"@electron/get": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-1.9.0.tgz",
"integrity": "sha512-OBIKtF6ttIJotDXe4KJMUyTBO4xMii+mFjlA8R4CORuD4HvCUaCK3lPjhdTRCvuEv6gzWNbAvd9DNBv0v780lw==",
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.0.tgz",
"integrity": "sha512-+SjZhRuRo+STTO1Fdhzqnv9D2ZhjxXP6egsJ9kiO8dtP68cDx7dFCwWi64dlMQV7sWcfW1OYCW4wviEBzmRsfQ==",
"dev": true,
"requires": {
"debug": "^4.1.1",
@ -1634,7 +1634,7 @@
"global-tunnel-ng": "^2.7.1",
"got": "^9.6.0",
"progress": "^2.0.3",
"sanitize-filename": "^1.6.2",
"semver": "^6.2.0",
"sumchecker": "^3.0.1"
},
"dependencies": {
@ -1648,6 +1648,12 @@
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
@ -3015,9 +3021,9 @@
"dev": true
},
"boolean": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.1.tgz",
"integrity": "sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.1.4.tgz",
"integrity": "sha512-3hx0kwU3uzG6ReQ3pnaFQPSktpBw6RHN3/ivDKEuU8g1XSfafowyvDnadjv1xp8IZqhtSukxlwv9bF6FhX8m0w==",
"dev": true,
"optional": true
},
@ -3298,9 +3304,9 @@
},
"dependencies": {
"get-stream": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
"integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"requires": {
"pump": "^3.0.0"
@ -3313,9 +3319,9 @@
"dev": true
},
"normalize-url": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz",
"integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==",
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
"integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
"dev": true
}
}
@ -3774,9 +3780,9 @@
}
},
"config-chain": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz",
"integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
"dev": true,
"optional": true,
"requires": {
@ -4799,9 +4805,9 @@
"dev": true
},
"electron": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-8.2.0.tgz",
"integrity": "sha512-mnV43gKCrCUMHLmGws/DU/l8LhaxrFD53A4ofwtthdCqOZWGIdk1+eMphiVumXR5a3lC64XVvmXQ2k28i7F/zw==",
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.4.0.tgz",
"integrity": "sha512-hOC4q0jkb+UDYZRy8vrZ1IANnq+jznZnbkD62OEo06nU+hIbp2IrwDRBNuSLmQ3cwZMVir0WSIA1qEVK0PkzGA==",
"dev": true,
"requires": {
"@electron/get": "^1.0.1",
@ -5007,9 +5013,9 @@
"dev": true
},
"env-paths": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz",
"integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"dev": true
},
"err-code": {
@ -6276,34 +6282,37 @@
}
},
"global-agent": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.1.8.tgz",
"integrity": "sha512-VpBe/rhY6Rw2VDOTszAMNambg+4Qv8j0yiTNDYEXXXxkUNGWLHp8A3ztK4YDBbFNcWF4rgsec6/5gPyryya/+A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.2.0.tgz",
"integrity": "sha512-+20KpaW6DDLqhG7JDiJpD1JvNvb8ts+TNl7BPOYcURqCrXqnN1Vf+XVOrkKJAFPqfX+oEhsdzOj1hLWkBTdNJg==",
"dev": true,
"optional": true,
"requires": {
"boolean": "^3.0.0",
"core-js": "^3.6.4",
"boolean": "^3.0.1",
"core-js": "^3.6.5",
"es6-error": "^4.1.1",
"matcher": "^2.1.0",
"roarr": "^2.15.2",
"semver": "^7.1.2",
"serialize-error": "^5.0.0"
"matcher": "^3.0.0",
"roarr": "^2.15.3",
"semver": "^7.3.2",
"serialize-error": "^7.0.1"
},
"dependencies": {
"core-js": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
"integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==",
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.3.tgz",
"integrity": "sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw==",
"dev": true,
"optional": true
},
"semver": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz",
"integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==",
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"optional": true
"optional": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
@ -6326,9 +6335,9 @@
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
"globalthis": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.1.tgz",
"integrity": "sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz",
"integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==",
"dev": true,
"optional": true,
"requires": {
@ -8427,19 +8436,19 @@
}
},
"matcher": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-2.1.0.tgz",
"integrity": "sha512-o+nZr+vtJtgPNklyeUKkkH42OsK8WAfdgaJE2FNxcjLPg+5QbeEoT6vRj8Xq/iv18JlQ9cmKsEu0b94ixWf1YQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"dev": true,
"optional": true,
"requires": {
"escape-string-regexp": "^2.0.0"
"escape-string-regexp": "^4.0.0"
},
"dependencies": {
"escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"optional": true
}
@ -10662,6 +10671,12 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
"dev": true
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@ -11717,13 +11732,13 @@
}
},
"roarr": {
"version": "2.15.2",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.2.tgz",
"integrity": "sha512-jmaDhK9CO4YbQAV8zzCnq9vjAqeO489MS5ehZ+rXmFiPFFE6B+S9KYO6prjmLJ5A0zY3QxVlQdrIya7E/azz/Q==",
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"dev": true,
"optional": true,
"requires": {
"boolean": "^3.0.0",
"boolean": "^3.0.1",
"detect-node": "^2.0.4",
"globalthis": "^1.0.1",
"json-stringify-safe": "^5.0.1",
@ -11810,15 +11825,6 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"sanitize-filename": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
"dev": true,
"requires": {
"truncate-utf8-bytes": "^1.0.0"
}
},
"sass": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.27.0.tgz",
@ -12033,19 +12039,19 @@
}
},
"serialize-error": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-5.0.0.tgz",
"integrity": "sha512-/VtpuyzYf82mHYTtI4QKtwHa79vAdU5OQpNPAmE/0UDdlGT0ZxHwC+J6gXkw29wwoVI8fMPsfcVHOwXtUQYYQA==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"dev": true,
"optional": true,
"requires": {
"type-fest": "^0.8.0"
"type-fest": "^0.13.1"
},
"dependencies": {
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
"optional": true
}
@ -13403,15 +13409,6 @@
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true
},
"truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
"integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=",
"dev": true,
"requires": {
"utf8-byte-length": "^1.0.1"
}
},
"ts-md5": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.2.7.tgz",
@ -13843,14 +13840,6 @@
"dev": true,
"requires": {
"prepend-http": "^2.0.0"
},
"dependencies": {
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
"dev": true
}
}
},
"use": {
@ -13865,12 +13854,6 @@
"integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=",
"dev": true
},
"utf8-byte-length": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
"integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=",
"dev": true
},
"util": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",

@ -61,7 +61,7 @@
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"codelyzer": "^6.0.0",
"electron": "^8.0.1",
"electron": "^9.4.0",
"eslint": "^7.32.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",

@ -118,6 +118,10 @@ export class AppComponent implements OnInit, AfterViewInit {
}
this.postsService.reloadCategories();
this.postsService.getVersionInfo().subscribe(res => {
this.postsService.version_info = res['version_info'];
});
}
// theme stuff

@ -107,6 +107,12 @@ export class RecentVideosComponent implements OnInit {
this.fileTypeFilter = cached_file_type_filter;
}
const sort_order = localStorage.getItem('recent_videos_sort_order');
if (sort_order) {
this.descendingMode = sort_order === 'descending';
}
this.searchChangedSubject
.debounceTime(500)
.pipe(distinctUntilChanged()
@ -145,6 +151,7 @@ export class RecentVideosComponent implements OnInit {
toggleModeChange() {
this.descendingMode = !this.descendingMode;
localStorage.setItem('recent_videos_sort_order', this.descendingMode ? 'descending' : 'ascending');
this.getAllFiles();
}
@ -195,7 +202,7 @@ export class RecentVideosComponent implements OnInit {
} else {
// normal subscriptions
!new_tab ? this.router.navigate(['/player', {uid: file.uid,
type: file.isAudio ? 'audio' : 'video', sub_id: sub.id}])
type: file.isAudio ? 'audio' : 'video'}])
: window.open(`/#/player;uid=${file.uid};type=${file.isAudio ? 'audio' : 'video'}`);
}
} else {

@ -21,6 +21,17 @@
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon>&nbsp;&nbsp;<ng-container *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag"><a [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container></ng-container>
<span *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] === current_version_tag">You are up to date.</span>
</p>
<p>
<ng-container i18n="Installation type">Installation type:</ng-container>&nbsp;{{postsService.version_info.type}}
<br>
<ng-container *ngIf="postsService.version_info.type === 'docker'">
<ng-container i18n="Docker tag">Docker tag:</ng-container>&nbsp;{{postsService.version_info.tag}}
<br>
</ng-container>
<ng-container i18n="Commit hash">Commit hash:</ng-container>&nbsp;{{postsService.version_info.commit}}
<br>
<ng-container i18n="Build date">Build date:</ng-container>&nbsp;{{postsService.version_info.date}}
</p>
<p>
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container>&nbsp;<a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a>&nbsp;<ng-container i18n="About bug suffix">to create an issue!</ng-container>
</p>

@ -19,7 +19,7 @@ export class AboutDialogComponent implements OnInit {
sidepanel_mode = this.postsService.sidepanel_mode;
card_size = this.postsService.card_size;
constructor(private postsService: PostsService) { }
constructor(public postsService: PostsService) { }
ngOnInit(): void {
this.getLatestGithubRelease();

@ -129,7 +129,9 @@ mat-form-field.mat-form-field {
}
.edit-button {
margin-left: 10px;
margin-left: 5px;
margin-top: -6px;
margin-bottom: -5px;
top: -5px;
}

@ -111,8 +111,13 @@
</ng-container>
</mat-checkbox>
<button class="edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-checkbox color="accent" [disabled]="!customArgsEnabled || current_download" (change)="replaceArgsChanged($event)" [(ngModel)]="replaceArgs" style="z-index: 999; margin-left: 10px" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Replace args">
Replace args
</ng-container>
</mat-checkbox>
<mat-form-field color="accent" style="margin-bottom: 42px;" class="advanced-input">
<input [(ngModel)]="customArgs" [ngModelOptions]="{standalone: true}" [disabled]="!customArgsEnabled" matInput placeholder="Custom args" i18n-placeholder="Custom args placeholder">
<input [(ngModel)]="customArgs" [ngModelOptions]="{standalone: true}" [disabled]="!customArgsEnabled" matInput (ngModelChange)="argChanged()" placeholder="Custom args" i18n-placeholder="Custom args placeholder">
<mat-hint>
<ng-container i18n="Custom Args input hint">
No need to include URL, just everything after. Args are delimited using two commas like so: ,,
@ -127,7 +132,7 @@
</ng-container>
</mat-checkbox>
<mat-form-field style="margin-bottom: 42px;" color="accent" class="advanced-input">
<input [(ngModel)]="customOutput" [ngModelOptions]="{standalone: true}" [disabled]="!customOutputEnabled" matInput placeholder="Custom output" i18n-placeholder="Custom output placeholder">
<input [(ngModel)]="customOutput" [ngModelOptions]="{standalone: true}" [disabled]="!customOutputEnabled" matInput (ngModelChange)="argChanged()" placeholder="Custom output" i18n-placeholder="Custom output placeholder">
<mat-hint><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Youtube-dl output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
@ -140,13 +145,13 @@
Use authentication
</ng-container>
</mat-checkbox>
<mat-form-field color="accent" class="advanced-input">
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Username" i18n-placeholder="YT Username placeholder">
<mat-form-field *ngIf="youtubeAuthEnabled" color="accent" class="advanced-input">
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argChanged()" placeholder="Username" i18n-placeholder="YT Username placeholder">
</mat-form-field>
</div>
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
<mat-form-field style="margin-top: 31px;" color="accent" class="advanced-input">
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Password" i18n-placeholder="YT Password placeholder">
<mat-form-field *ngIf="youtubeAuthEnabled" style="margin-top: 31px;" color="accent" class="advanced-input">
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argChanged()" placeholder="Password" i18n-placeholder="YT Password placeholder">
</mat-form-field>
</div>
<div class="col-12 col-sm-6 mt-3">
@ -155,13 +160,13 @@
Crop file
</ng-container>
</mat-checkbox>
<mat-form-field color="accent" class="advanced-input">
<input [(ngModel)]="cropFileStart" type="number" [ngModelOptions]="{standalone: true}" [disabled]="!cropFile" matInput placeholder="Crop from (seconds)" i18n-placeholder="Crop from placeholder">
<mat-form-field *ngIf="cropFile" color="accent" class="advanced-input">
<input [(ngModel)]="cropFileStart" type="number" [ngModelOptions]="{standalone: true}" matInput placeholder="Crop from (seconds)" i18n-placeholder="Crop from placeholder">
</mat-form-field>
</div>
<div class="col-12 col-sm-6 mt-3">
<mat-form-field style="margin-top: 31px;" color="accent" class="advanced-input">
<input [(ngModel)]="cropFileEnd" type="number" [ngModelOptions]="{standalone: true}" [disabled]="!cropFile" matInput placeholder="Crop to (seconds)" i18n-placeholder="Crop to placeholder">
<mat-form-field *ngIf="cropFile" style="margin-top: 31px;" color="accent" class="advanced-input">
<input [(ngModel)]="cropFileEnd" type="number" [ngModelOptions]="{standalone: true}" matInput placeholder="Crop to (seconds)" i18n-placeholder="Crop to placeholder">
</mat-form-field>
</div>
</div>

@ -1,7 +1,7 @@
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList } from '@angular/core';
import {PostsService} from '../posts.services';
import {FileCardComponent} from '../file-card/file-card.component';
import { Observable } from 'rxjs';
import { Observable, Subject } from 'rxjs';
import {FormControl, Validators} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
@ -9,7 +9,6 @@ import { saveAs } from 'file-saver';
import { YoutubeSearchService, Result } from '../youtube-search.service';
import { Router, ActivatedRoute } from '@angular/router';
import { Platform } from '@angular/cdk/platform';
import { v4 as uuid } from 'uuid';
import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
import { RecentVideosComponent } from 'app/components/recent-videos/recent-videos.component';
@ -50,6 +49,7 @@ export class MainComponent implements OnInit {
customArgsEnabled = false;
customArgs = null;
customOutputEnabled = false;
replaceArgs = false;
customOutput = null;
youtubeAuthEnabled = false;
youtubeUsername = null;
@ -212,6 +212,7 @@ export class MainComponent implements OnInit {
error: false
};
argsChangedSubject: Subject<boolean> = new Subject<boolean>();
simulatedOutput = '';
constructor(public postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
@ -224,8 +225,6 @@ export class MainComponent implements OnInit {
if (this.autoStartDownload) {
this.downloadClicked();
}
setInterval(() => this.getSimulatedOutput(), 1000);
}
async loadConfig() {
@ -261,6 +260,10 @@ export class MainComponent implements OnInit {
this.customOutputEnabled = localStorage.getItem('customOutputEnabled') === 'true';
}
if (localStorage.getItem('replaceArgs') !== null) {
this.replaceArgs = localStorage.getItem('replaceArgs') === 'true';
}
if (localStorage.getItem('youtubeAuthEnabled') !== null) {
this.youtubeAuthEnabled = localStorage.getItem('youtubeAuthEnabled') === 'true';
}
@ -323,6 +326,12 @@ export class MainComponent implements OnInit {
this.autoStartDownload = true;
}
this.argsChangedSubject
.debounceTime(500)
.subscribe((should_simulate) => {
if (should_simulate) this.getSimulatedOutput();
});
this.setCols();
}
@ -412,7 +421,8 @@ export class MainComponent implements OnInit {
this.urlError = false;
// get common args
const customArgs = (this.customArgsEnabled ? this.customArgs : null);
const customArgs = (this.customArgsEnabled && this.replaceArgs ? this.customArgs : null);
const additionalArgs = (this.customArgsEnabled && !this.replaceArgs ? this.customArgs : null);
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null);
const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null);
@ -445,7 +455,7 @@ export class MainComponent implements OnInit {
this.downloadingfile = true;
this.postsService.downloadFile(this.url, type, (this.selectedQuality === '' ? null : this.selectedQuality),
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, cropFileSettings).subscribe(res => {
customQualityConfiguration, customArgs, additionalArgs, customOutput, youtubeUsername, youtubePassword, cropFileSettings).subscribe(res => {
this.current_download = res['download'];
this.downloads.push(res['download']);
this.download_uids.push(res['download']['uid']);
@ -593,6 +603,7 @@ export class MainComponent implements OnInit {
if (str !== this.last_valid_url && this.allowQualitySelect) {
// get info
this.getURLInfo(str);
this.argsChangedSubject.next(true);
}
this.last_valid_url = str;
}
@ -630,79 +641,44 @@ export class MainComponent implements OnInit {
}
}
getSimulatedOutput() {
const customArgsExists = this.customArgsEnabled && this.customArgs;
const globalArgsExists = this.globalCustomArgs && this.globalCustomArgs !== '';
let full_string_array: string[] = [];
const base_string_array = ['youtube-dl', this.url];
if (customArgsExists) {
this.simulatedOutput = base_string_array.join(' ') + ' ' + this.customArgs.split(',,').join(' ');
return this.simulatedOutput;
argChanged(): void {
this.argsChangedSubject.next(true);
}
full_string_array.push(...base_string_array);
const base_path = this.audioOnly ? this.audioFolderPath : this.videoFolderPath;
const ext = this.audioOnly ? '.mp3' : '.mp4';
// gets output
let output_string_array = ['-o', base_path + '%(title)s' + ext];
if (this.customOutputEnabled && this.customOutput) {
output_string_array = ['-o', base_path + this.customOutput + ext];
}
// before pushing output, should check if using an external downloader
if (!this.useDefaultDownloadingAgent && this.customDownloadingAgent === 'aria2c') {
full_string_array.push('--external-downloader', 'aria2c');
}
// pushes output
full_string_array.push(...output_string_array);
getSimulatedOutput(): void {
// this function should be very similar to downloadFile()
const customArgs = (this.customArgsEnabled && this.replaceArgs ? this.customArgs : null);
const additionalArgs = (this.customArgsEnabled && !this.replaceArgs ? this.customArgs : null);
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null);
const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null);
// logic splits into audio and video modes
if (this.audioOnly) {
// adds base audio string
const format_array = [];
const audio_format = this.getSelectedAudioFormat();
if (audio_format) {
format_array.push('-f', audio_format);
} else if (this.selectedQuality) {
format_array.push('--audio-quality', this.selectedQuality['format_id']);
}
const type = this.audioOnly ? 'audio' : 'video';
// pushes formats
full_string_array.splice(2, 0, ...format_array);
const customQualityConfiguration = type === 'audio' ? this.getSelectedAudioFormat() : this.getSelectedVideoFormat();
const additional_params = ['-x', '--audio-format', 'mp3', '--write-info-json', '--print-json'];
let cropFileSettings = null;
full_string_array.push(...additional_params);
} else {
// adds base video string
let format_array = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'];
const video_format = this.getSelectedVideoFormat();
if (video_format) {
format_array = ['-f', video_format];
} else if (this.selectedQuality) {
format_array = [`bestvideo[height=${this.selectedQuality['format_id']}]+bestaudio/best[height=${this.selectedQuality}]`];
if (this.cropFile) {
cropFileSettings = {
cropFileStart: this.cropFileStart,
cropFileEnd: this.cropFileEnd
}
// pushes formats
full_string_array.splice(2, 0, ...format_array);
const additional_params = ['--write-info-json', '--print-json'];
full_string_array.push(...additional_params);
}
if (this.use_youtubedl_archive) {
full_string_array.push('--download-archive', 'archive.txt');
this.postsService.generateArgs(this.url, type, (this.selectedQuality === '' ? null : this.selectedQuality),
customQualityConfiguration, customArgs, additionalArgs, customOutput, youtubeUsername, youtubePassword, cropFileSettings).subscribe(res => {
const simulated_args = res['args'];
if (simulated_args) {
// hide password if needed
const passwordIndex = simulated_args.indexOf('--password');
console.log(passwordIndex);
if (passwordIndex !== -1 && passwordIndex !== simulated_args.length - 1) {
simulated_args[passwordIndex + 1] = simulated_args[passwordIndex + 1].replace(/./g, '*');
}
if (globalArgsExists) {
full_string_array = full_string_array.concat(this.globalCustomArgs.split(',,'));
this.simulatedOutput = `youtube-dl ${this.url} ${simulated_args.join(' ')}`;
}
this.simulatedOutput = full_string_array.join(' ');
return this.simulatedOutput;
});
}
errorFormats(url) {
@ -746,6 +722,7 @@ export class MainComponent implements OnInit {
videoModeChanged(new_val) {
this.selectedQuality = '';
localStorage.setItem('audioOnly', new_val.checked.toString());
this.argsChangedSubject.next(true);
}
autoplayChanged(new_val) {
@ -754,29 +731,22 @@ export class MainComponent implements OnInit {
customArgsEnabledChanged(new_val) {
localStorage.setItem('customArgsEnabled', new_val.checked.toString());
if (new_val.checked === true && this.customOutputEnabled) {
this.customOutputEnabled = false;
localStorage.setItem('customOutputEnabled', 'false');
this.youtubeAuthEnabled = false;
localStorage.setItem('youtubeAuthEnabled', 'false');
this.argsChangedSubject.next(true);
}
replaceArgsChanged(new_val) {
localStorage.setItem('replaceArgs', new_val.checked.toString());
this.argsChangedSubject.next(true);
}
customOutputEnabledChanged(new_val) {
localStorage.setItem('customOutputEnabled', new_val.checked.toString());
if (new_val.checked === true && this.customArgsEnabled) {
this.customArgsEnabled = false;
localStorage.setItem('customArgsEnabled', 'false');
}
this.argsChangedSubject.next(true);
}
youtubeAuthEnabledChanged(new_val) {
localStorage.setItem('youtubeAuthEnabled', new_val.checked.toString());
if (new_val.checked === true && this.customArgsEnabled) {
this.customArgsEnabled = false;
localStorage.setItem('customArgsEnabled', 'false');
}
this.argsChangedSubject.next(true);
}
getAudioAndVideoFormats(formats) {

@ -60,6 +60,7 @@ export class PostsService implements CanActivate {
categories = null;
sidenav = null;
locale = isoLangs['en'];
version_info = null;
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
public snackBar: MatSnackBar, private titleService: Title) {
@ -174,11 +175,25 @@ export class PostsService implements CanActivate {
}
// tslint:disable-next-line: max-line-length
downloadFile(url: string, type: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, cropFileSettings = null) {
downloadFile(url: string, type: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, additionalArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, cropFileSettings = null) {
return this.http.post(this.path + 'downloadFile', {url: url,
selectedHeight: selectedQuality,
customQualityConfiguration: customQualityConfiguration,
customArgs: customArgs,
additionalArgs: additionalArgs,
customOutput: customOutput,
youtubeUsername: youtubeUsername,
youtubePassword: youtubePassword,
type: type,
cropFileSettings: cropFileSettings}, this.httpOptions);
}
generateArgs(url: string, type: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, additionalArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, cropFileSettings = null) {
return this.http.post(this.path + 'generateArgs', {url: url,
selectedHeight: selectedQuality,
customQualityConfiguration: customQualityConfiguration,
customArgs: customArgs,
additionalArgs: additionalArgs,
customOutput: customOutput,
youtubeUsername: youtubeUsername,
youtubePassword: youtubePassword,
@ -453,6 +468,10 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'clearFinishedDownloads', {}, this.httpOptions);
}
getVersionInfo() {
return this.http.get(this.path + 'versionInfo', this.httpOptions);
}
updateServer(tag) {
return this.http.post(this.path + 'updateServer', {tag: tag}, this.httpOptions);
}

@ -1,12 +1,5 @@
<h4 class="settings-title" i18n="Settings title">Settings</h4>
<!-- <ng-container i18n="Allow subscriptions setting"></ng-container> -->
<!-- Language
<div style="margin-bottom: 10px;">
</div> -->
<mat-tab-group style="height: 76vh" mat-align-tabs="center">
<mat-tab-group style="height: 76vh" mat-align-tabs="center" [selectedIndex]="tabIndex" (selectedTabChange)="tabChanged($event)">
<!-- Server -->
<mat-tab label="Main" i18n-label="Main settings label">
<ng-template matTabContent style="padding: 15px;">
@ -272,9 +265,12 @@
<mat-hint><ng-container i18n="Twitch API Key setting hint AKA preamble">Also known as a Client ID.</ng-container>&nbsp;<a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-4 mb-3">
<div class="col-12 mt-4">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2 mb-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['generate_NFO_files']" matTooltip="Generates NFO files with every download, primarily used by Kodi." i18n-matTooltip="Generate NFO files tooltip"><ng-container i18n="Generate NFO files setting">Generate NFO files</ng-container></mat-checkbox>
</div>
</div>
</div>
<mat-divider></mat-divider>
@ -423,7 +419,13 @@
</div>
</ng-template>
</mat-tab>
<mat-tab *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode" label="Users" i18n-label="Users settings label">
<mat-tab [disabled]="!postsService.config?.Advanced.multi_user_mode">
<ng-template mat-tab-label>
<div [matTooltip]="!postsService.config?.Advanced.multi_user_mode ? usersTabDisabledTooltip : null">
<ng-container i18n="Users settings label">Users</ng-container>
</div>
</ng-template>
<ng-container *ngIf="postsService.config?.Advanced.multi_user_mode">
<div *ngIf="new_config" style="margin-top: 24px; margin-bottom: -25px;">
<div>
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
@ -469,6 +471,7 @@
<mat-divider></mat-divider>
</div>
<app-modify-users *ngIf="new_config"></app-modify-users>
</ng-container>
</mat-tab>
<mat-tab *ngIf="postsService.config" label="Logs" i18n-label="Logs settings label">
<ng-template matTabContent>

@ -12,6 +12,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo
import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/edit-category-dialog.component';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-settings',
@ -20,7 +21,7 @@ import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/ed
})
export class SettingsComponent implements OnInit {
all_locales = isoLangs;
supported_locales = ['en', 'es', 'de', 'fr', 'nl', 'pt', 'it', 'ca', 'cs', 'nb', 'ru', 'zh', 'id', 'en-GB'];
supported_locales = ['en', 'es', 'de', 'fr', 'nl', 'pt', 'it', 'ca', 'cs', 'nb', 'ru', 'zh', 'ko', 'id', 'en-GB'];
initialLocale = localStorage.getItem('locale');
initial_config = null;
@ -38,17 +39,28 @@ export class SettingsComponent implements OnInit {
latestGithubRelease = null;
CURRENT_VERSION = CURRENT_VERSION
get settingsAreTheSame() {
tabs = ['main', 'downloader', 'extra', 'database', 'advanced', 'users', 'logs'];
tabIndex = 0;
INDEX_TO_TAB = Object.assign({}, this.tabs);
TAB_TO_INDEX = {};
usersTabDisabledTooltip = $localize`You must enable multi-user mode to access this tab.`;
get settingsAreTheSame(): boolean {
this._settingsSame = this.settingsSame()
return this._settingsSame;
}
set settingsAreTheSame(val) {
set settingsAreTheSame(val: boolean) {
this._settingsSame = val;
}
constructor(public postsService: PostsService, private snackBar: MatSnackBar, private sanitizer: DomSanitizer,
private dialog: MatDialog) { }
private dialog: MatDialog, private router: Router, private route: ActivatedRoute) {
// invert index to tab
Object.keys(this.INDEX_TO_TAB).forEach(key => { this.TAB_TO_INDEX[this.INDEX_TO_TAB[key]] = key; });
}
ngOnInit() {
if (this.postsService.initialized) {
@ -66,6 +78,9 @@ export class SettingsComponent implements OnInit {
this.generated_bookmarklet_code = this.sanitizer.bypassSecurityTrustUrl(this.generateBookmarkletCode());
this.getLatestGithubRelease();
const tab = this.route.snapshot.paramMap.get('tab');
this.tabIndex = tab && this.TAB_TO_INDEX[tab] ? this.TAB_TO_INDEX[tab] : 0;
}
getConfig() {
@ -98,6 +113,11 @@ export class SettingsComponent implements OnInit {
this.new_config = JSON.parse(JSON.stringify(this.initial_config));
}
tabChanged(event) {
const index = event['index'];
this.router.navigate(['/settings', {tab: this.INDEX_TO_TAB[index]}]);
}
dropCategory(event: CdkDragDrop<string[]>) {
moveItemInArray(this.postsService.categories, event.previousIndex, event.currentIndex);
this.postsService.updateCategories(this.postsService.categories).subscribe(res => {

File diff suppressed because it is too large Load Diff

@ -0,0 +1,261 @@
{
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "대하여",
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "프로필",
"adb4562d2dbd3584370e44496969d58c511ecb63": "다크",
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "설정",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "홈",
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "로그인",
"357064ca9d9ac859eb618e28e8126fa32be049e2": "구독",
"822fab38216f64e8166d368b59fe756ca39d301b": "다운로드",
"4a9889d36910edc8323d7bab60858ab3da6d91df": "오디오만",
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "다운로드",
"a38ae1082fec79ba1f379978337385a539a28e73": "품질",
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "URL 이용",
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "보기",
"96a01fafe135afc58b0f8071a4ab00234495ce18": "복수 다운로드 모드",
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "취소",
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "고급",
"4e4c721129466be9c3862294dc40241b64045998": "사용자 지정 인수 이용",
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "사용자 지정 인수",
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "URL을 포함할 필요가 없습니다. 이후의 모든 항목만 포함하면 됩니다. 인수는 다음과 같은 두 개의 쉼표를 사용하여 구분됩니다. : ,,",
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "사용자 지정 출력 사용",
"d9c02face477f2f9cdaae318ccee5f89856851fb": "사용자 지정 출력",
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "문서",
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "경로는 설정된 다운로드 경로에 상대적입니다. 확장자는 포함하지 마세요.",
"4e1291cb1d579e7b7a1b802e6a8fd16ef7a557fa": "파일 자르기",
"44d007f6f8a2b19f12d85f9e49647b4ac02d7cbe": "자르기 시작지점 (초)",
"661206c3ab91fa81e9d8b40afb29f1866b78432f": "자르기 마무리지점 (초)",
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "시뮬레이션된 명령:",
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "인증 사용",
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "아이디",
"c32ef07f8803a223a83ed17024b38e8d82292407": "비밀번호",
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "재생목록 만들기",
"cff1428d10d59d14e45edec3c735a27b5482db59": "제목",
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "종류",
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "오디오",
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "동영상",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "오디오 파일",
"a52dae09be10ca3a65da918533ced3d3f4992238": "동영상",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "재생목록이나 채널 구독",
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "재생목록이나 채널 URL",
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "사용자 지정 이름",
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "모든 업로드 된 파일 다운로드",
"d641b8fa5ac5e85114c733b1f7de6976bd091f70": "최고 화질",
"c76a955642714b8949ff3e4b4990864a2e2cac95": "오디오 전용 모드",
"408ca4911457e84a348cecf214f02c69289aa8f1": "스트리밍 전용 모드",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "이것들은 일반적인 인수 뒤에 추가됩니다.",
"98b6ec9ec138186d663e64770267b67334353d63": "사용자 지정 파일 출력",
"d7b35c384aecd25a516200d6921836374613dfe7": "취소",
"d0336848b0c375a1c25ba369b3481ee383217a4f": "구독",
"28a678e9cabf86e44c32594c43fa0e890135c20f": "마지막으로 업로드된 동영상 다운로드",
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "종류:",
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "아이디:",
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "닫기",
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "아카이브 내보내기",
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "구독 취소",
"303e45ffae995c9817e510e38cb969e6bb3adcbf": "(일시정지)",
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "아카이브:",
"616e206cb4f25bd5885fc35925365e43cf5fb929": "제목:",
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "업로더:",
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "파일 크기:",
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "경로:",
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "업로드 날짜:",
"0cc1dec590ecd74bef71a865fb364779bc42a749": "카테고리:",
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Youtube-dl 인수 수정",
"7fc1946abe2b40f60059c6cd19975d677095fd19": "시뮬레이션된 새 인수",
"0b71824ae71972f236039bed43f8d2323e8fd570": "인수 추가",
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "카테고리로 찾기",
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "인수 값 이용",
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "인수 추가",
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "수정",
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "인수 값",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "업데이터",
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "사용자 등록",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "아이디",
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "등록",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "새 쿠키 업로드",
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "참고: 새로운 쿠키를 추가하면 이전 쿠키를 덮어씁니다. 또한 쿠키는 사용자 개인이 아닌 전체에 적용됩니다.",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "드래그 앤 드롭",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "재생목록 수정",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "저장",
"cba36d610ddba59b6dd6fbec77199eabf0ff2de3": "재생할 때 재생목록 섞기",
"5caadefa4143cf6766a621b0f54f91f373a1f164": "콘텐츠 추가",
"33026f57ea65cd9c8a5d917a08083f71a718933a": "기본 순서",
"29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "순서 거꾸로",
"d02888c485d3aeab6de628508f4a00312a722894": "내 동영상",
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "검색",
"73423607944a694ce6f9e55cfee329681bb4d9f9": "동영상 없음.",
"3697f8583ea42868aa269489ad366103d94aece7": "수정중",
"07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "일시정지됨",
"c3b0b86523f1d10e84a71f9b188d54913a11af3b": "카테고리 수정중",
"2489eefea00931942b91f4a1ae109514b591e2e1": "규칙",
"e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "새로운 규칙 추가",
"792dc6a57f28a1066db283f2e736484f066005fd": "트위치 채팅 다운로드",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "수정",
"826b25211922a1b46436589233cb6f1a163d89b7": "삭제",
"321e4419a943044e674beb55b8039f42a9761ca5": "정보",
"e684046d73bcee88e82f7ff01e2852789a05fc32": "동영상 수:",
"34504b488c24c27e68089be549f0eeae6ebaf30b": "삭제하고 블랙리스트 추가",
"dad95154dcef3509b8cc705046061fd24994bbb7": "조회수",
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "성공적으로 다운로드 완료",
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "에러 발생",
"4f8b2bb476981727ab34ed40fde1218361f92c45": "세부사항",
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "에러 발생:",
"77b0c73840665945b25bd128709aa64c8f017e1c": "다운로드 시작:",
"08ff9375ec078065bcdd7637b7ea65fce2979266": "다운로드 끝:",
"ad127117f9471612f47d01eae09709da444a36a4": "파일 경로(들):",
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "구독중",
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "채널",
"47546e45bbb476baaaad38244db444c427ddc502": "재생목록",
"29b89f751593e1b347eef103891b7a1ff36ec03f": "이름이 유효하지 않음. 채널 검색중.",
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "구독중인 채널이 없습니다.",
"2e0a410652cb07d069f576b61eab32586a18320d": "이름이 유효하지 않음. 플레이리스트 검색중.",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "구독중인 재생목록이 없습니다.",
"82421c3e46a0453a70c42900eab51d58d79e6599": "메인",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "다운로더",
"d5f69691f9f05711633128b5a3db696783266b58": "추가",
"fb324ec7da611c6283caa6fc6257c39a56d6aaf7": "데이터베이스",
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "고급",
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "사용자",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "로그",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {닫다} false {취소} other {기타}}",
"54c512cca1923ab72faf1a0bd98d3d172469629a": "포트를 제외한 이 앱에 접속할 URL.",
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "포트",
"22e8f1d0423a3b784fe40fab187b92c06541b577": "포트 설정. 기본 포트는 17442 입니다.",
"d4477669a560750d2064051a510ef4d7679e2f3e": "복수 사용자 모드",
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "사용자 기본 경로",
"a64505c41150663968e277ec9b3ddaa5f4838798": "사용자와 그들의 동영상 다운로드를 위한 기본 경로.",
"4e3120311801c4acd18de7146add2ee4a4417773": "구독 허용",
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "구독 기본 경로",
"bc9892814ee2d119ae94378c905ea440a249b84a": "구독된 채널과 재생목록에서 나온 영상들을 위한 기본 경로. 경로는 YTDL-Material 루트 폴더 경로에 상대적입니다.",
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "확인 간격",
"0f56a7449b77630c114615395bbda4cab398efd8": "단위는 초이며, 숫자만 넣으세요.",
"13759b09a7f4074ceee8fa2f968f9815fdf63295": "가끔 새 동영상이 최고 화질 처리 전에 다운로드 될 때가 있습니다. 이 설정은 새 동영상이 더 높은 화질의 버전이 있는지 다음 날짜에 확인됨을 의미합니다.",
"3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "높은 화질 재다운로드",
"27a56aad79d8b61269ed303f11664cc78bcc2522": "테마",
"ff7cee38a2259526c519f878e71b964f41db4348": "기본",
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "테마 변경 허용",
"fe46ccaae902ce974e2441abe752399288298619": "언어",
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "오디오 폴더 경로",
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "오디오 전용 다운로드 경로. 경로는 YTDL-Material 루트 폴더 경로에 상대적입니다.",
"46826331da1949bd6fb74624447057099c9d20cd": "동영상 폴더 경로",
"17c92e6d47a213fa95b5aa344b3f258147123f93": "동영상 다운로드 경로. 경로는 YTDL-Material 루트 폴더 경로에 상대적입니다.",
"cfe829634b1144bc44b6d38cf5584ea65db9804f": "기본 파일 출력",
"1148fd45287ff09955b938756bc302042bcb29c7": "경로는 위의 다운로드 경로에 상대적입니다. 확장자는 포함하지 마세요.",
"ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "전반적으로 적용될 사용자 지정 인수",
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "홈페이지에서의 다운로드에 대해 전반적으로 적용될 사용자 지정 인수. 인수는 다음과 같은 두 개의 쉼표를 사용하여 구분됩니다. : ,,",
"04201f9d27abd7d6f58a4328ab98063ce1072006": "카테고리",
"1f6d3986a970af27f16f8a95ce0dc3033cc90a83": "이 설정을 사용하면, 하나의 동영상이 카테고리와 일치할 경우, 전체 재생목록에 해당 카테고리가 표시됩니다.",
"5da94ccb2301f586af26916e921bdad6d673ab58": "재생목록 카테고리화 허용",
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Youtube-dl 아카이브 사용",
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "썸네일 포함",
"384de8f8f112c9e6092eb2698706d391553f3e8d": "메타데이터 포함",
"fb35145bfb84521e21b6385363d59221f436a573": "모든 다운로드 종료",
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "상위 제목",
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "파일 매니저 설정됨",
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "다운로드 매니저 설정됨",
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "화질 선택 허용",
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "다운로드 전용 모드",
"09d31c803a7252658694e1e3176b97f5655a3fe3": "복수 다운로드 모드 허용",
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "오픈 API 허용",
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "오픈 API 키",
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "문서 보기",
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "이것은 예전 API키를 지울 것입니다!",
"1b258b258b4cc475ceb2871305b61756b0134f4a": "생성",
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "유튜브 API 사용",
"ce10d31febb3d9d60c160750570310f303a22c22": "유튜브 API 키",
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "키를 만드는 것은 쉽습니다!",
"d162f9fcd6a7187b391e004f072ab3da8377c47d": "트위치 API 사용",
"8ae23bc4302a479f687f4b20a84c276182e2519c": "트위치 API 키",
"84ffcebac2709ca0785f4a1d5ba274433b5beabc": "클라이언트 ID라고도 알려져 있음.",
"5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "트위치 채팅 자동 다운로드",
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "이곳을 누르세요",
"7f09776373995003161235c0c8d02b7f91dbc4df": "공식 YoutubeDL-Material 크롬 확장 프로그램을 수동으로 다운로드 하기 위해.",
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "반드시 확장 프로그램을 수동으로 실행하고 확장 프로그램 설정을 수정하여 프론트엔드 URL을 설정해야 합니다.",
"9a2ec6da48771128384887525bdcac992632c863": "파이어폭스 확장 프로그램 페이지에서 바로 공식 YoutubeDL-Material 파이어폭스 확장 프로그램을 설치하기 위해.",
"eb81be6b49e195e5307811d1d08a19259d411f37": "자세한 설정 지침.",
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "프론트엔드 URL을 설정하기 위해 확장 프로그램 설정을 변경하는 것 외에는 필요한 것이 많지 않습니다.",
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "아래 링크를 북마크에 끌어다 놓으시면 됩니다! 이제 그냥 다운로드하고자 하는 유튜브 비디오 페이지에서 북마크를 클릭하면 됩니다.",
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "'오디오 전용' 북마크 생성",
"47955e2cc6986625528b4352034858180d675281": "데이터베이스 위치:",
"9f8de81d44ec2a9a58b97e589b9e3154b3966c60": "테이블당 레코드",
"3913164a51898aac444bf6c7150e46ad5a8a18ad": "몽고DB 연결 문자열",
"5473e36f5102e2ae22ce4c6620cacc40cc98da95": "예시:",
"d54142de169844b014ae913a4056c31495f4a305": "연결 문자열 테스트",
"98e94c9bdac1ca8beb29d73b2e6f7a9e5e035aec": "DB 전환",
"b1c08387975e6feada407c9b5f5f564261b8192b": "데이터베이스 정보를 검색할 수 없습니다. 자세한 내용은 서버 로그를 확인하세요.",
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "다운로더 선택",
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "기본 다운로드 에이전트 사용",
"c776eb4992b6c98f58cd89b20c1ea8ac37888521": "다운로드 에이전트 선택",
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "로그 레벨",
"db6c192032f4cab809aad35215f0aa4765761897": "로그인 만료",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "고급 다운로드 허용",
"431e5f3a0dde88768d1074baedd65266412b3f02": "쿠키 사용",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "쿠키 설정",
"635285fa5624d50a408feb7eb564c0db0d3f1ce1": "서버 재시작",
"37224420db54d4bc7696f157b779a7225f03ca9d": "사용자 등록 허용",
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "인증 방법",
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "내부",
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
"1db9789b93069861019bd0ccaa5d4706b00afc61": "LDAP URL",
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Bind DN",
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Bind Credentials",
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "기본 검색",
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "검색 필터",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "YoutubeDL-Material에 대하여",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "은(는) 구글의 Material 디자인 요건에 따라 만들어진 오픈소스 유튜브 다운로더 입니다. 당신은 당신이 좋아하는 동영상을 동영상이나 오디오 파일로 원활하게 받을 수 있으며, 심지어 당신이 좋아하는 채널이나 재생목록을 구독해 그들의 새로운 동영상을 지속적으로 업데이트 할 수도 있습니다.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "은(는) 광범위한 API, 도커 지원, 현지화 (번역) 지원을 포함한 몇몇 엄청난 기능이 포함되어 있습니다! 아래 깃허브 아이콘을 클릭해 모든 지원되는 기능을 확인해보세요.",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "설치된 버전:",
"b33536f59b94ec935a16bd6869d836895dc5300c": "버그를 찾았거나 제안하실 사항이 있으신가요?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "이슈를 생성하기 위해!",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "업데이트 확인중...",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "업데이트 가능",
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "설정 메뉴에서 업데이트를 할 수 있습니다.",
"1372e61c5bd06100844bd43b98b016aabc468f62": "선택된 버전:",
"1f6d14a780a37a97899dc611881e6bc971268285": "공유 허용",
"6580b6a950d952df847cb3d8e7176720a740adc8": "타임스탬프 사용",
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "초",
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "클립보드에 복사",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "재생목록 공유",
"94e2674467c7a08a291f9bd97ce694d4e47ffd62": "파일 공유",
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "세션 아이디:",
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "모든 다운로드된 항목 지우기",
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(현재)",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "다운로드된 항목 없음!",
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "프로필",
"bb694b49d408265c91c62799c2b3a7e3151c824d": "로그아웃",
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "생성됨:",
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "로그인하지 않았습니다.",
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "관리자 계정 생성",
"2d2adf3ca26a676bca2269295b7455a26fd26980": "기본 관리자 계정이 감지되지 않았습니다. 이것은 'admin'이라는 ID를 가진 관리자 계정을 만들고, 비밀번호를 설정할 것입니다.",
"70a67e04629f6d412db0a12d51820b480788d795": "생성",
"4d92a0395dd66778a931460118626c5794a3fc7a": "사용자 추가",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "역할 수정",
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "ID",
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "역할",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "액션",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "사용자 관리",
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "사용자 삭제",
"632e8b20c98e8eec4059a605a4b011bb476137af": "사용자 수정",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "사용자 UID:",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "새 비밀번호",
"6498fa1b8f563988f769654a75411bb8060134b9": "새 비밀번호 설정",
"544e09cdc99a8978f48521d45f62db0da6dcf742": "기본 역할 사용",
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "네",
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "아니오",
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "역할 관리",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "줄:",
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "로그 지우기",
"24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "자동으로 생성됨",
"ccf5ea825526ac490974336cb5c24352886abc07": "파일 열기",
"5656a06f17c24b2d7eae9c221567b209743829a9": "새 탭에서 파일 열기",
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "구독중으로 가기",
"94e01842dcee90531caa52e4147f70679bac87fe": "삭제하고 재다운로드",
"2031adb51e07a41844e8ba7704b054e98345c9c1": "영원히 삭제",
"ddc31f2885b1b33a7651963254b0c197f2a64086": "더 보기.",
"56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "간략히 보기.",
"2054791b822475aeaea95c0119113de3200f5e1c": "길이:"
}
Loading…
Cancel
Save