Compare commits

...

24 Commits

Author SHA1 Message Date
Isaac Abadi
84187b9474 Fixed issue where selecting video quality would
Main component cleanup

Removed deprecated file card component
2021-09-28 20:14:57 -06:00
Isaac Abadi
dbeeb32d48 Updated Dockerfile and entrypoint to use pm2 instead of forever 2021-09-27 18:11:38 -06:00
Isaac Abadi
46087f622e Switched forever.js to pm2
Updated winston
2021-09-26 02:40:05 -06:00
Isaac Abadi
5dd48035fb Improved archive management for subscription downloads
Downloads that fail due to existing in the archive now appears as an error in the manager

Fixed issue where redownloading sub videos wouldn't occur if it was not cleared from the download manager
2021-09-25 22:33:22 -06:00
Isaac Abadi
db53a12635 Added Korean translations and updated source translations file 2021-09-22 21:29:15 -06:00
Tzahi12345
8cd21bf433 Merge pull request #322 from Tzahi12345/dependabot/npm_and_yarn/electron-9.4.0
Bump electron from 8.2.0 to 9.4.0
2021-09-22 20:24:53 -06:00
Tzahi12345
c33acfb3de Merge pull request #447 from Tzahi12345/dependabot/npm_and_yarn/backend/axios-0.21.2
Bump axios from 0.21.1 to 0.21.2 in /backend
2021-09-22 20:24:43 -06:00
Isaac Abadi
562eaa1b9b Added support for generate NFO files for Kodi
Minor UI updates to settings
2021-09-22 19:27:25 -06:00
Isaac Abadi
ec7f04552f Fixed mangled Subject initialization in main component 2021-09-21 23:56:04 -06:00
Isaac Abadi
75fc09ed99 Improved arg simulation -- now uses same method as the actual download
Added checkbox for advanced custom args to either replace all args or append
2021-09-21 23:51:07 -06:00
Isaac Abadi
8aa354ac24 Fixed issue where navigating to a sub's video would play all videos from the subscription 2021-09-21 20:05:09 -06:00
Isaac Abadi
58a0dc4afe Version and commit info is now generated during autobuilds and can be viewed in the about dialog
Prepared removal of JSON translations from repo to move towards XLIFF-only
2021-09-21 20:05:09 -06:00
Glassed Silver
0e37d83740 Merge pull request #455 from GlassedSilver/Readme-Update
Readme update
2021-09-20 00:29:23 +02:00
Isaac Abadi
27faff054e Recent videos component now remembers sort order between page reloads 2021-09-19 14:56:32 -04:00
Isaac Abadi
a71d9f5c7e Added tests for arg generation and laid some plumbing for better arg simulation in the UI 2021-09-19 14:44:02 -04:00
Isaac Abadi
759637c1cf Fixed issue where per-subscription custom args were not being applied 2021-09-19 14:29:12 -04:00
Isaac Abadi
33f23c3ca9 Fixed issue where youtube-dl autoupdates broke if checkExistsWithTimeout failed the first time 2021-09-19 14:24:18 -04:00
GlassedSilver
176c99f813 Reference host-specific instructions 2021-09-18 17:41:25 +02:00
GlassedSilver
f7e0b3e86b [DRAFT!] Bump alpine: pinned '3.12' → 'latest' 2021-09-18 17:22:11 +02:00
GlassedSilver
bd2443b1e9 docker-compose.yml: Use YoutubeDL-Material nightly 2021-09-18 16:59:49 +02:00
Isaac Abadi
f689609941 Fixed missing rxjs import 2021-09-17 01:15:08 -04:00
dependabot[bot]
1e9eec1b55 Bump electron from 8.2.0 to 9.4.0
Bumps [electron](https://github.com/electron/electron) from 8.2.0 to 9.4.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v8.2.0...v9.4.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-16 19:36:01 +00:00
dependabot[bot]
677af3712b Bump axios from 0.21.1 to 0.21.2 in /backend
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-16 19:35:35 +00:00
Tzahi12345
5fd9d93007 Merge pull request #420 from Tzahi12345/download-manager
Download manager
2021-09-16 15:34:41 -04:00
41 changed files with 4125 additions and 950 deletions

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
FROM alpine:3.12 as frontend
FROM alpine:latest as frontend
RUN apk add --no-cache \
npm
@@ -15,14 +15,13 @@ RUN ng build --prod
#--------------#
FROM alpine:3.12
FROM alpine:latest
ENV UID=1000 \
GID=1000 \
USER=youtube
ENV NO_UPDATE_NOTIFIER=true
ENV FOREVER_ROOT=/app/.forever
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
@@ -37,7 +36,7 @@ RUN apk add --no-cache \
WORKDIR /app
COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
RUN npm install forever -g
RUN npm install pm2 -g
RUN npm install && chown -R $UID:$GID ./
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
@@ -45,4 +44,4 @@ COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "forever", "app.js" ]
CMD [ "pm2-runtime", "pm2.config.js" ]

View File

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

View File

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

View File

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

View File

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

View File

@@ -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': {

View File

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

View File

@@ -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);
}
@@ -278,7 +279,9 @@ async function downloadQueuedFile(download_uid) {
} else if (output) {
if (output.length === 0 || output[0].length === 0) {
// ERROR!
logger.warn(`No output received for video download, check if it exists in your archive.`)
const error_message = `No output received for video download, check if it exists in your archive.`;
await handleDownloadError(download_uid, error_message);
logger.warn(error_message);
resolve(false);
return;
}
@@ -328,6 +331,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);
}
@@ -339,9 +346,10 @@ async function downloadQueuedFile(download_uid) {
}
if (options.merged_string !== null && options.merged_string !== undefined) {
let current_merged_archive = fs.readFileSync(path.join(fileFolderPath, `merged_${type}.txt`), 'utf8');
let diff = current_merged_archive.replace(options.merged_string, '');
const archive_path = download['user_uid'] ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`);
const archive_folder = getArchiveFolder(fileFolderPath, options, download['user_uid']);
const current_merged_archive = fs.readFileSync(path.join(archive_folder, `merged_${type}.txt`), 'utf8');
const diff = current_merged_archive.replace(options.merged_string, '');
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
fs.appendFileSync(archive_path, diff);
}
@@ -369,7 +377,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');
@@ -409,7 +417,7 @@ async function generateArgs(url, type, options, user_uid = null) {
downloadConfig = customArgs.split(',,');
} else {
if (customQualityConfiguration) {
qualityPath = ['-f', customQualityConfiguration];
qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4'];
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
} else if (is_audio) {
@@ -450,23 +458,16 @@ async function generateArgs(url, type, options, user_uid = null) {
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) {
let archive_folder = null;
if (options.customArchivePath) {
archive_folder = path.join(options.customArchivePath);
} else if (user_uid) {
archive_folder = path.join(fileFolderPath, 'archives');
} else {
archive_folder = path.join(archivePath);
}
const archive_folder = getArchiveFolder(fileFolderPath, options, user_uid);
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
await fs.ensureDir(archive_folder);
await fs.ensureFile(archive_path);
let blacklist_path = path.join(archive_folder, `blacklist_${type}.txt`);
const blacklist_path = path.join(archive_folder, `blacklist_${type}.txt`);
await fs.ensureFile(blacklist_path);
let merged_path = path.join(fileFolderPath, `merged_${type}.txt`);
const merged_path = path.join(archive_folder, `merged_${type}.txt`);
await fs.ensureFile(merged_path);
// merges blacklist and regular archive
let inputPathList = [archive_path, blacklist_path];
@@ -510,7 +511,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;
}
@@ -603,4 +604,29 @@ async function checkDownloadPercent(download_uid) {
const percent_complete = (sum_size/resulting_file_size * 100).toFixed(2);
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);
}
function getArchiveFolder(fileFolderPath, options, user_uid) {
if (options.customArchivePath) {
return path.join(options.customArchivePath);
} else if (user_uid) {
return path.join(fileFolderPath, 'archives');
} else {
return path.join(archivePath);
}
}

View File

@@ -0,0 +1,8 @@
module.exports = {
apps : [{
name : "YoutubeDL-Material",
script : "./app.js",
watch : "placeholder",
watch_delay: 5000
}]
}

View File

@@ -1,7 +1,7 @@
#!/bin/sh
set -eu
CMD="forever app.js"
CMD="pm2-runtime pm2.config.js"
# if the first arg starts with "-" pass it to program
if [ "${1#-}" != "$1" ]; then

View File

@@ -4,6 +4,48 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@dabh/diagnostics": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz",
"integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==",
"requires": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"@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 +358,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": {
@@ -693,11 +735,6 @@
"simple-swizzle": "^0.2.2"
}
},
"colornames": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz",
"integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y="
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
@@ -951,16 +988,6 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"diagnostics": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz",
"integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==",
"requires": {
"colorspace": "1.1.x",
"enabled": "1.0.x",
"kuler": "1.0.x"
}
},
"dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
@@ -1072,12 +1099,9 @@
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
},
"enabled": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz",
"integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=",
"requires": {
"env-variable": "0.0.x"
}
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"encodeurl": {
"version": "1.0.2",
@@ -1092,11 +1116,6 @@
"once": "^1.4.0"
}
},
"env-variable": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz",
"integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg=="
},
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -1117,6 +1136,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",
@@ -1201,15 +1225,10 @@
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"fast-safe-stringify": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
"integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
},
"fecha": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
"integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg=="
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz",
"integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q=="
},
"fill-range": {
"version": "7.0.1",
@@ -1266,10 +1285,15 @@
}
}
},
"fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"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",
@@ -1784,12 +1808,9 @@
}
},
"kuler": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz",
"integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==",
"requires": {
"colornames": "^1.1.1"
}
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"latest-version": {
"version": "5.1.0",
@@ -1995,21 +2016,21 @@
}
},
"logform": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz",
"integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.3.0.tgz",
"integrity": "sha512-graeoWUH2knKbGthMtuG1EfaSPMZFZBIrhuJHhkS5ZseFBrc7DupCzihOQAzsK/qIKPQaPJ/lFQFctILUY5ARQ==",
"requires": {
"colors": "^1.2.1",
"fast-safe-stringify": "^2.0.4",
"fecha": "^2.3.3",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^1.1.0",
"triple-beam": "^1.3.0"
},
"dependencies": {
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
@@ -2473,9 +2494,12 @@
}
},
"one-time": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz",
"integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"requires": {
"fn.name": "1.x.x"
}
},
"onetime": {
"version": "5.1.0",
@@ -2871,6 +2895,21 @@
"glob": "^7.1.3"
}
},
"rxjs": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz",
"integrity": "sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==",
"requires": {
"tslib": "~2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A=="
}
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -2882,6 +2921,11 @@
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
"optional": true
},
"safe-stable-stringify": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz",
"integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -3015,6 +3059,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",
@@ -3472,42 +3521,27 @@
}
},
"winston": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz",
"integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz",
"integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==",
"requires": {
"async": "^2.6.1",
"diagnostics": "^1.1.1",
"is-stream": "^1.1.0",
"logform": "^2.1.1",
"one-time": "0.0.4",
"readable-stream": "^3.1.1",
"@dabh/diagnostics": "^2.0.2",
"async": "^3.1.0",
"is-stream": "^2.0.0",
"logform": "^2.2.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.3.0"
},
"dependencies": {
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"requires": {
"lodash": "^4.17.14"
}
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
}
"winston-transport": "^4.4.0"
}
},
"winston-transport": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz",
"integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz",
"integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==",
"requires": {
"readable-stream": "^2.3.6",
"readable-stream": "^2.3.7",
"triple-beam": "^1.2.0"
},
"dependencies": {
@@ -3578,6 +3612,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",

View File

@@ -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",
@@ -61,10 +61,12 @@
"progress": "^2.0.3",
"ps-node": "^0.1.6",
"read-last-lines": "^1.7.2",
"rxjs": "^7.3.0",
"shortid": "^2.2.15",
"unzipper": "^0.10.10",
"uuidv4": "^6.0.6",
"winston": "^3.2.1",
"winston": "^3.3.3",
"xmlbuilder2": "^3.0.2",
"youtube-dl": "^3.0.2"
}
}

7
backend/pm2.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
apps : [{
name : "YoutubeDL-Material",
script : "./app.js",
watch : "placeholder"
}]
}

View File

@@ -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;
@@ -430,7 +431,7 @@ async function getFilesToDownload(sub, output_jsons) {
const files_to_download = [];
for (let i = 0; i < output_jsons.length; i++) {
const output_json = output_jsons[i];
const file_missing = !(await db_api.getRecord('files', {sub_id: sub.id, url: output_json['webpage_url']})) && !(await db_api.getRecord('download_queue', {sub_id: sub.id, url: output_json['webpage_url'], error: null}));
const file_missing = !(await db_api.getRecord('files', {sub_id: sub.id, url: output_json['webpage_url']})) && !(await db_api.getRecord('download_queue', {sub_id: sub.id, url: output_json['webpage_url'], error: null, finished: false}));
if (file_missing) {
const file_with_path_exists = await db_api.getRecord('files', {sub_id: sub.id, path: output_json['_filename']});
if (file_with_path_exists) {
@@ -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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -34,7 +34,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { PostsService } from 'app/posts.services';
import { FileCardComponent } from './file-card/file-card.component';
import { RouterModule } from '@angular/router';
import { AppRoutingModule } from './app-routing.module';
import { MainComponent } from './main/main.component';
@@ -98,7 +97,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
@NgModule({
declarations: [
AppComponent,
FileCardComponent,
MainComponent,
PlayerComponent,
InputDialogComponent,

View File

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

View File

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

View File

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

View File

@@ -1,68 +0,0 @@
.example-card {
width: 150px;
height: 125px;
padding: 0px;
}
.deleteButton {
top:-5px;
right:-5px;
position:absolute;
}
/* Coerce the <span> icon container away from display:inline */
.mat-icon-button .mat-button-wrapper {
display: flex;
justify-content: center;
}
.image {
width: 100%;
}
.example-full-width-height {
width: 100%;
height: 100%
}
.centered {
margin: 0 auto;
top: 50%;
left: 50%;
}
.img-div {
height: 60px;
padding: 0px;
margin: 8px 0px 0px -5px;
width: calc(100% + 5px + 5px);
overflow: hidden;
border-radius: 0px 0px 4px 4px;
}
.max-two-lines {
display: -webkit-box;
display: -moz-box;
max-height: 2.4em;
line-height: 1.2em;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.file-link {
width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
@media (max-width: 576px){
.example-card {
width: 125px !important;
}
}

View File

@@ -1,29 +0,0 @@
<mat-card class="example-card mat-elevation-z6">
<div style="padding:5px">
<div style="height: 52px;">
<div>
<b><a class="file-link" href="javascript:void(0)" (click)="!playlist ? mainComponent.goToFile(name, isAudio, uid) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
</div>
<span class="max-two-lines"><ng-container i18n="File or playlist ID">ID:</ng-container>&nbsp;{{name}}</span>
<div *ngIf="playlist"><ng-container i18n="Playlist video count">Count:</ng-container>&nbsp;{{count}}</div>
</div>
<div *ngIf="!image_errored && thumbnailURL" class="img-div">
<img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail">
<span *ngIf="!image_loaded">
</span>
</div>
</div>
<button [matMenuTriggerFor]="playlist_menu" *ngIf="playlist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #playlist_menu="matMenu">
<button (click)="editPlaylistDialog()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
<button (click)="deleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
</mat-menu>
<button [matMenuTriggerFor]="action_menu" *ngIf="!playlist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #action_menu="matMenu">
<button (click)="openVideoInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
<button (click)="deleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="use_youtubedl_archive" (click)="deleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
</mat-menu>
</mat-card>

View File

@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FileCardComponent } from './file-card.component';
describe('FileCardComponent', () => {
let component: FileCardComponent;
let fixture: ComponentFixture<FileCardComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ FileCardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FileCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,120 +0,0 @@
import { Component, OnInit, Input, Output } from '@angular/core';
import {PostsService} from '../posts.services';
import { MatSnackBar } from '@angular/material/snack-bar';
import {EventEmitter} from '@angular/core';
import { MainComponent } from 'app/main/main.component';
import { Subject, Observable } from 'rxjs';
import 'rxjs/add/observable/merge';
import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
import { ModifyPlaylistComponent } from '../dialogs/modify-playlist/modify-playlist.component';
@Component({
selector: 'app-file-card',
templateUrl: './file-card.component.html',
styleUrls: ['./file-card.component.css']
})
export class FileCardComponent implements OnInit {
@Input() file: any;
@Input() title: string;
@Input() length: string;
@Input() name: string;
@Input() uid: string;
@Input() thumbnailURL: string;
@Input() isAudio = true;
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
@Input() playlist = null;
@Input() count = null;
@Input() use_youtubedl_archive = false;
type;
image_loaded = false;
image_errored = false;
scrollSubject;
scrollAndLoad;
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent,
private dialog: MatDialog) {
this.scrollSubject = new Subject();
this.scrollAndLoad = Observable.merge(
Observable.fromEvent(window, 'scroll'),
this.scrollSubject
);
}
ngOnInit() {
this.type = this.isAudio ? 'audio' : 'video';
if (this.file && this.file.url && this.file.url.includes('youtu')) {
const string_id = (this.playlist ? '?list=' : '?v=')
const index_offset = (this.playlist ? 6 : 3);
const end_index = this.file.url.indexOf(string_id) + index_offset;
this.name = this.file.url.substring(end_index, this.file.url.length);
}
}
deleteFile(blacklistMode = false) {
if (!this.playlist) {
this.postsService.deleteFile(this.uid, blacklistMode).subscribe(result => {
if (result) {
this.openSnackBar('Delete success!', 'OK.');
this.removeFile.emit(this.name);
} else {
this.openSnackBar('Delete failed!', 'OK.');
}
}, err => {
this.openSnackBar('Delete failed!', 'OK.');
});
} else {
this.removeFile.emit(this.name);
}
}
openVideoInfoDialog() {
const dialogRef = this.dialog.open(VideoInfoDialogComponent, {
data: {
file: this.file,
},
minWidth: '50vw'
});
}
editPlaylistDialog() {
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
data: {
playlist_id: this.playlist.id,
width: '65vw'
}
});
dialogRef.afterClosed().subscribe(res => {
// updates playlist in file manager if it changed
if (dialogRef.componentInstance.playlist_updated) {
this.playlist = dialogRef.componentInstance.original_playlist;
this.title = this.playlist.name;
this.count = this.playlist.fileNames.length;
}
});
}
onImgError(event) {
this.image_errored = true;
}
onHoverResponse() {
this.scrollSubject.next();
}
imageLoaded(loaded) {
this.image_loaded = true;
}
public openSnackBar(message: string, action: string) {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

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

View File

@@ -19,7 +19,7 @@
Quality
</ng-container>
</mat-label>
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality" (ngModelChange)="argsChanged($event)">
<mat-option [value]="''">
Max
</mat-option>
@@ -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)="argsChanged()" 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)="argsChanged()" 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)="argsChanged()" 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)="argsChanged()" 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>

View File

@@ -1,7 +1,6 @@
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 +8,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 +48,7 @@ export class MainComponent implements OnInit {
customArgsEnabled = false;
customArgs = null;
customOutputEnabled = false;
replaceArgs = false;
customOutput = null;
youtubeAuthEnabled = false;
youtubeUsername = null;
@@ -90,7 +89,6 @@ export class MainComponent implements OnInit {
mp3s: any[] = [];
mp4s: any[] = [];
files_cols = null;
playlists = {'audio': [], 'video': []};
playlist_thumbnails = {};
downloading_content = {'audio': {}, 'video': {}};
@@ -197,8 +195,6 @@ export class MainComponent implements OnInit {
@ViewChild('urlinput', { read: ElementRef }) urlInput: ElementRef;
@ViewChild('recentVideos') recentVideos: RecentVideosComponent;
@ViewChildren('audiofilecard') audioFileCards: QueryList<FileCardComponent>;
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
last_valid_url = '';
last_url_check = 0;
@@ -212,6 +208,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,
@@ -219,16 +216,14 @@ export class MainComponent implements OnInit {
this.audioOnly = false;
}
async configLoad() {
async configLoad(): Promise<void> {
await this.loadConfig();
if (this.autoStartDownload) {
this.downloadClicked();
}
setInterval(() => this.getSimulatedOutput(), 1000);
}
async loadConfig() {
async loadConfig(): Promise<boolean> {
// loading config
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled']
&& this.postsService.hasPermission('filemanager');
@@ -261,6 +256,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';
}
@@ -286,7 +285,7 @@ export class MainComponent implements OnInit {
}
// app initialization.
ngOnInit() {
ngOnInit(): void {
if (this.postsService.initialized) {
this.configLoad();
} else {
@@ -323,61 +322,22 @@ export class MainComponent implements OnInit {
this.autoStartDownload = true;
}
this.setCols();
this.argsChangedSubject
.debounceTime(500)
.subscribe((should_simulate) => {
if (should_simulate) this.getSimulatedOutput();
});
}
ngAfterViewInit() {
ngAfterViewInit(): void {
if (this.youtubeSearchEnabled && this.youtubeAPIKey) {
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
this.attachToInput();
}
}
public setCols() {
if (window.innerWidth <= 350) {
this.files_cols = 1;
} else if (window.innerWidth <= 500) {
this.files_cols = 2;
} else if (window.innerWidth <= 750) {
this.files_cols = 3
} else {
this.files_cols = 4;
}
}
public goToFile(container, isAudio, uid) {
this.downloadHelper(container, isAudio ? 'audio' : 'video', false, false, true);
}
public goToPlaylist(playlistID, type) {
const playlist = this.getPlaylistObjectByID(playlistID, type);
if (playlist) {
if (this.downloadOnlyMode) {
this.downloading_content[type][playlistID] = true;
this.downloadPlaylist(playlist);
} else {
localStorage.setItem('player_navigator', this.router.url);
const fileNames = playlist.fileNames;
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]);
}
} else {
// playlist not found
console.error(`Playlist with ID ${playlistID} not found!`);
}
}
getPlaylistObjectByID(playlistID, type) {
for (let i = 0; i < this.playlists[type].length; i++) {
const playlist = this.playlists[type][i];
if (playlist.id === playlistID) {
return playlist;
}
}
return null;
}
// download helpers
downloadHelper(container, type, is_playlist = false, force_view = false, navigate_mode = false) {
downloadHelper(container, type: string, is_playlist = false, force_view = false, navigate_mode = false): void {
this.downloadingfile = false;
if (!this.autoplay && !this.downloadOnlyMode && !navigate_mode) {
// do nothing
@@ -403,7 +363,7 @@ export class MainComponent implements OnInit {
}
// download click handler
downloadClicked() {
downloadClicked(): void {
if (!this.ValidURL(this.url)) {
this.urlError = true;
return;
@@ -412,7 +372,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);
@@ -443,16 +404,19 @@ export class MainComponent implements OnInit {
}
}
const selected_quality = this.selectedQuality;
this.selectedQuality = '';
this.downloadingfile = true;
this.postsService.downloadFile(this.url, type, (this.selectedQuality === '' ? null : this.selectedQuality),
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, cropFileSettings).subscribe(res => {
this.postsService.downloadFile(this.url, type, (selected_quality === '' ? null : selected_quality),
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']);
}, error => { // can't access server
}, () => { // can't access server
this.downloadingfile = false;
this.current_download = null;
this.openSnackBar('Download failed!', 'OK.');
this.postsService.openSnackBar('Download failed!', 'OK.');
});
if (!this.autoplay) {
@@ -464,7 +428,7 @@ export class MainComponent implements OnInit {
}
// download canceled handler
cancelDownload(download_to_cancel = null) {
cancelDownload(download_to_cancel = null): void {
// if one is provided, cancel that one. otherwise, remove the current one
if (download_to_cancel) {
this.removeDownloadFromCurrentDownloads(download_to_cancel)
@@ -475,33 +439,32 @@ export class MainComponent implements OnInit {
this.current_download = null;
}
getSelectedAudioFormat() {
getSelectedAudioFormat(): string {
if (this.selectedQuality === '') { return null; }
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
if (cachedFormatsExists) {
const audio_formats = this.cachedAvailableFormats[this.url]['formats']['audio'];
return this.selectedQuality['format_id'];
} else {
return null;
}
}
getSelectedVideoFormat() {
getSelectedVideoFormat(): string {
if (this.selectedQuality === '') { return null; }
const cachedFormats = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
if (cachedFormats) {
const video_formats = cachedFormats['video'];
if (this.selectedQuality) {
let selected_video_format = this.selectedQuality['format_id'];
// add in audio format if necessary
if (!this.selectedQuality['acodec'] && cachedFormats['best_audio_format']) selected_video_format += `+${cachedFormats['best_audio_format']}`;
const audio_missing = !this.selectedQuality['acodec'] || this.selectedQuality['acodec'] === 'none';
if (audio_missing && cachedFormats['best_audio_format']) selected_video_format += `+${cachedFormats['best_audio_format']}`;
return selected_video_format;
}
}
return null;
}
getDownloadByUID(uid) {
getDownloadByUID(uid: string) {
const index = this.downloads.findIndex(download => download.uid === uid);
if (index !== -1) {
return this.downloads[index];
@@ -510,7 +473,7 @@ export class MainComponent implements OnInit {
}
}
removeDownloadFromCurrentDownloads(download_to_remove) {
removeDownloadFromCurrentDownloads(download_to_remove): boolean {
if (this.current_download === download_to_remove) {
this.current_download = null;
}
@@ -523,7 +486,7 @@ export class MainComponent implements OnInit {
}
}
downloadFileFromServer(file, type) {
downloadFileFromServer(file, type: string): void {
const ext = type === 'audio' ? 'mp3' : 'mp4'
this.downloading_content[type][file.id] = true;
this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
@@ -533,13 +496,12 @@ export class MainComponent implements OnInit {
if (!this.fileManagerEnabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(file.uid).subscribe(delRes => {
});
this.postsService.deleteFile(file.uid).subscribe(() => {});
}
});
}
downloadPlaylist(playlist) {
downloadPlaylist(playlist): void {
this.postsService.downloadPlaylistFromServer(playlist.id).subscribe(res => {
if (playlist.id) { this.downloading_content[playlist.type][playlist.id] = false };
const blob: Blob = res;
@@ -548,25 +510,25 @@ export class MainComponent implements OnInit {
}
clearInput() {
clearInput(): void {
this.url = '';
this.results_showing = false;
}
onInputBlur() {
onInputBlur(): void {
this.results_showing = false;
}
visitURL(url) {
visitURL(url: string): void {
window.open(url);
}
useURL(url) {
useURL(url: string): void {
this.results_showing = false;
this.url = url;
}
inputChanged(new_val) {
inputChanged(new_val: string): void {
if (new_val === '' || !new_val) {
this.results_showing = false;
} else {
@@ -577,7 +539,7 @@ export class MainComponent implements OnInit {
}
// checks if url is a valid URL
ValidURL(str) {
ValidURL(str: string): boolean {
// tslint:disable-next-line: max-line-length
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
const re = new RegExp(strRegex);
@@ -593,20 +555,14 @@ export class MainComponent implements OnInit {
if (str !== this.last_valid_url && this.allowQualitySelect) {
// get info
this.getURLInfo(str);
this.argsChanged();
}
this.last_valid_url = str;
}
return valid;
}
// snackbar helper
public openSnackBar(message: string, action: string) {
this.snackBar.open(message, action, {
duration: 2000,
});
}
getURLInfo(url) {
getURLInfo(url: string): void {
// if url is a youtube playlist, skip getting url info
if (url.includes('playlist')) {
return;
@@ -624,93 +580,54 @@ export class MainComponent implements OnInit {
return;
}
this.cachedAvailableFormats[url]['formats'] = this.getAudioAndVideoFormats(infos.formats);
}, err => {
}, () => {
this.errorFormats(url);
});
}
}
getSimulatedOutput() {
const customArgsExists = this.customArgsEnabled && this.customArgs;
const globalArgsExists = this.globalCustomArgs && this.globalCustomArgs !== '';
getSimulatedOutput(): void {
// this function should be very similar to downloadClicked()
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);
let full_string_array: string[] = [];
const base_string_array = ['youtube-dl', this.url];
const type = this.audioOnly ? 'audio' : 'video';
if (customArgsExists) {
this.simulatedOutput = base_string_array.join(' ') + ' ' + this.customArgs.split(',,').join(' ');
return this.simulatedOutput;
}
const customQualityConfiguration = type === 'audio' ? this.getSelectedAudioFormat() : this.getSelectedVideoFormat();
full_string_array.push(...base_string_array);
let cropFileSettings = null;
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);
// 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']);
if (this.cropFile) {
cropFileSettings = {
cropFileStart: this.cropFileStart,
cropFileEnd: this.cropFileEnd
}
// pushes formats
full_string_array.splice(2, 0, ...format_array);
const additional_params = ['-x', '--audio-format', 'mp3', '--write-info-json', '--print-json'];
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}]`];
}
// 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');
}
if (globalArgsExists) {
full_string_array = full_string_array.concat(this.globalCustomArgs.split(',,'));
}
this.simulatedOutput = full_string_array.join(' ');
return this.simulatedOutput;
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, '*');
}
this.simulatedOutput = `youtube-dl ${this.url} ${simulated_args.join(' ')}`;
}
});
}
errorFormats(url) {
errorFormats(url: string): void {
this.cachedAvailableFormats[url]['formats_loading'] = false;
console.error('Could not load formats for url ' + url);
}
attachToInput() {
attachToInput(): void {
Observable.fromEvent(this.urlInput.nativeElement, 'keyup')
.map((e: any) => e.target.value) // extract the value of input
.filter((text: string) => text.length > 1) // filter out if empty
@@ -739,47 +656,41 @@ export class MainComponent implements OnInit {
);
}
onResize(event) {
this.setCols();
argsChanged(): void {
this.argsChangedSubject.next(true);
}
videoModeChanged(new_val) {
videoModeChanged(new_val): void {
this.selectedQuality = '';
localStorage.setItem('audioOnly', new_val.checked.toString());
this.argsChanged();
}
autoplayChanged(new_val) {
autoplayChanged(new_val): void {
localStorage.setItem('autoplay', new_val.checked.toString());
}
customArgsEnabledChanged(new_val) {
customArgsEnabledChanged(new_val): void {
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.argsChanged();
}
customOutputEnabledChanged(new_val) {
replaceArgsChanged(new_val): void {
localStorage.setItem('replaceArgs', new_val.checked.toString());
this.argsChanged();
}
customOutputEnabledChanged(new_val): void {
localStorage.setItem('customOutputEnabled', new_val.checked.toString());
if (new_val.checked === true && this.customArgsEnabled) {
this.customArgsEnabled = false;
localStorage.setItem('customArgsEnabled', 'false');
}
this.argsChanged();
}
youtubeAuthEnabledChanged(new_val) {
youtubeAuthEnabledChanged(new_val): void {
localStorage.setItem('youtubeAuthEnabled', new_val.checked.toString());
if (new_val.checked === true && this.customArgsEnabled) {
this.customArgsEnabled = false;
localStorage.setItem('customArgsEnabled', 'false');
}
this.argsChanged();
}
getAudioAndVideoFormats(formats) {
getAudioAndVideoFormats(formats): void {
const audio_formats: any = {};
const video_formats: any = {};
@@ -838,7 +749,7 @@ export class MainComponent implements OnInit {
return parsed_formats;
}
getBestAudioFormatForMp4(audio_formats) {
getBestAudioFormatForMp4(audio_formats): void {
let best_audio_format_for_mp4 = null;
let best_audio_format_bitrate = 0;
const available_audio_format_keys = Object.keys(audio_formats);
@@ -854,46 +765,8 @@ export class MainComponent implements OnInit {
return best_audio_format_for_mp4;
}
accordionEntered(type) {
if (type === 'audio') {
audioFilesMouseHovering = true;
this.audioFileCards.forEach(filecard => {
filecard.onHoverResponse();
});
} else if (type === 'video') {
videoFilesMouseHovering = true;
this.videoFileCards.forEach(filecard => {
filecard.onHoverResponse();
});
}
}
accordionLeft(type) {
if (type === 'audio') {
audioFilesMouseHovering = false;
} else if (type === 'video') {
videoFilesMouseHovering = false;
}
}
accordionOpened(type) {
if (type === 'audio') {
audioFilesOpened = true;
} else if (type === 'video') {
videoFilesOpened = true;
}
}
accordionClosed(type) {
if (type === 'audio') {
audioFilesOpened = false;
} else if (type === 'video') {
videoFilesOpened = false;
}
}
// modify custom args
openArgsModifierDialog() {
openArgsModifierDialog(): void {
const dialogRef = this.dialog.open(ArgModifierDialogComponent, {
data: {
initial_args: this.customArgs
@@ -906,7 +779,7 @@ export class MainComponent implements OnInit {
});
}
getCurrentDownload() {
getCurrentDownload(): void {
if (!this.current_download) {
return;
}
@@ -923,7 +796,7 @@ export class MainComponent implements OnInit {
} else if (this.current_download['finished'] && this.current_download['error']) {
this.downloadingfile = false;
this.current_download = null;
this.openSnackBar('Download failed!', 'OK.');
this.postsService.openSnackBar('Download failed!', 'OK.');
}
} else {
// console.log('failed to get new download');
@@ -931,7 +804,7 @@ export class MainComponent implements OnInit {
});
}
reloadRecentVideos() {
reloadRecentVideos(): void {
this.postsService.files_changed.next(true);
}
}

View File

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

View File

@@ -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,52 +419,59 @@
</div>
</ng-template>
</mat-tab>
<mat-tab *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode" label="Users" i18n-label="Users settings label">
<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>
<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>
<mat-divider></mat-divider>
<mat-form-field style="margin-top: 15px;">
<mat-select [(ngModel)]="new_config['Users']['auth_method']" placeholder="Auth method" i18n-placeholder="Auth method select">
<mat-option value="internal">
<ng-container i18n="Internal auth method">Internal</ng-container>
</mat-option>
<mat-option value="ldap">
<ng-container i18n="LDAP auth method">LDAP</ng-container>
</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="new_config['Users']['auth_method'] === 'ldap'">
</ng-template>
<ng-container *ngIf="postsService.config?.Advanced.multi_user_mode">
<div *ngIf="new_config" style="margin-top: 24px; margin-bottom: -25px;">
<div>
<mat-form-field>
<input matInput i18n-placeholder="LDAP URL" placeholder="LDAP URL" [(ngModel)]="new_config['Users']['ldap_config']['url']">
</mat-form-field>
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Bind DN" placeholder="Bind DN" [(ngModel)]="new_config['Users']['ldap_config']['bindDN']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Bind Credentials" placeholder="Bind Credentials" [(ngModel)]="new_config['Users']['ldap_config']['bindCredentials']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Search Base" placeholder="Search Base" [(ngModel)]="new_config['Users']['ldap_config']['searchBase']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Search Filter" placeholder="Search Filter" [(ngModel)]="new_config['Users']['ldap_config']['searchFilter']">
</mat-form-field>
<mat-divider></mat-divider>
<mat-form-field style="margin-top: 15px;">
<mat-select [(ngModel)]="new_config['Users']['auth_method']" placeholder="Auth method" i18n-placeholder="Auth method select">
<mat-option value="internal">
<ng-container i18n="Internal auth method">Internal</ng-container>
</mat-option>
<mat-option value="ldap">
<ng-container i18n="LDAP auth method">LDAP</ng-container>
</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="new_config['Users']['auth_method'] === 'ldap'">
<div>
<mat-form-field>
<input matInput i18n-placeholder="LDAP URL" placeholder="LDAP URL" [(ngModel)]="new_config['Users']['ldap_config']['url']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Bind DN" placeholder="Bind DN" [(ngModel)]="new_config['Users']['ldap_config']['bindDN']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Bind Credentials" placeholder="Bind Credentials" [(ngModel)]="new_config['Users']['ldap_config']['bindCredentials']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Search Base" placeholder="Search Base" [(ngModel)]="new_config['Users']['ldap_config']['searchBase']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Search Filter" placeholder="Search Filter" [(ngModel)]="new_config['Users']['ldap_config']['searchFilter']">
</mat-form-field>
</div>
</div>
<mat-divider></mat-divider>
</div>
<mat-divider></mat-divider>
</div>
<app-modify-users *ngIf="new_config"></app-modify-users>
<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>

View File

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

View File

@@ -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": "길이:"
}

File diff suppressed because it is too large Load Diff