Compare commits

...

24 Commits

Author SHA1 Message Date
Tzahi12345
d3b88412c6 Fixed thumbnails for auto-generated playlists 2023-04-23 22:27:09 -04:00
Tzahi12345
6cee892e18 Added label for category field in video-info-dialog 2023-04-23 22:20:50 -04:00
Tzahi12345
e2438a236b Adjusted category UI styling to Angular 15 updates 2023-04-23 22:14:20 -04:00
Tzahi12345
955c401f0b Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into categories-fix 2023-04-22 17:47:16 -04:00
Tzahi12345
527b1f1cb9 Merge pull request #872 from Tzahi12345/twitch-downloader-fix
Twitch chat download fixes
2023-04-21 00:16:06 -04:00
Tzahi12345
24d8072eb5 Fixed minor syntax error in Dockerfile 2023-04-20 21:47:20 -04:00
Tzahi12345
c81bf980ca Separated image for TwitchDownloader download in Dockerfile 2023-04-20 21:41:36 -04:00
Tzahi12345
a91381720f Added link in error message to get TiwtchDownloaderCLI 2023-04-20 21:16:06 -04:00
Tzahi12345
edd4a0928c Fixed twitch downloading by using TiwtchDownloader's CLI
Removed unecessary settings

Created dedicated folder for docker utils
2023-04-20 21:11:48 -04:00
Tzahi12345
770916492e Fixed authentication error in notifications (2) 2023-04-17 23:56:52 -04:00
Tzahi12345
6400b807c2 Fixed auth error in notifications 2023-04-17 23:54:34 -04:00
Tzahi12345
3a7e2d9d0f Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2023-04-16 21:09:35 -04:00
Tzahi12345
ca5381fe0f Updated tasks DB-related code to not insert properties that prevent local_db from being imported
Added DB functionality to remove properties from records

DB records in local DB can now be updated if nested
2023-04-16 21:08:18 -04:00
Glassed Silver
bd8d91ebe5 Merge pull request #862 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2023-04-16 14:05:13 +02:00
Kawaxte
27f05dbae3 Translated using Weblate (Estonian)
Currently translated at 31.8% (153 of 480 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/et/
2023-04-16 13:51:39 +02:00
Glassed Silver
c7bf1d0e27 Merge pull request #786 from beauharrison/docker-custom-user-startup
Fixed long docker startup time by optimizing chown use
2023-04-16 10:30:24 +02:00
Glassed Silver
57be0a032e Merge pull request #858 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2023-04-16 07:50:35 +02:00
Glassed Silver
6fe4b22efc Merge pull request #860 from Tzahi12345/docker-fix
Docker fixes
2023-04-16 07:48:50 +02:00
Kawaxte
af2d583924 Added translation using Weblate (Estonian) 2023-04-15 12:14:14 +02:00
yangyangdaji
c61d51be76 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (480 of 480 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2023-04-14 16:49:59 +02:00
Beau Harrison
142d708ee3 Fixed long docker startup time by optimizing chown use 2022-11-17 08:16:24 +10:00
Isaac Abadi
2e52ec22e0 Default sort for videos is now download date 2022-09-15 21:38:19 -04:00
Isaac Abadi
efdd0dd228 Categories fix for yt-dlp 2022-09-15 21:38:00 -04:00
Isaac Abadi
415c97cb09 Fixed issue where categories were not being properly applied to matching files (#701) 2022-07-07 01:07:22 -04:00
24 changed files with 5484 additions and 430 deletions

View File

@@ -2,7 +2,7 @@
FROM ubuntu:22.04 AS ffmpeg
ENV DEBIAN_FRONTEND=noninteractive
# Use script due local build compability
COPY ffmpeg-fetch.sh .
COPY docker-utils/ffmpeg-fetch.sh .
RUN chmod +x ffmpeg-fetch.sh
RUN sh ./ffmpeg-fetch.sh
@@ -47,6 +47,15 @@ RUN npm config set strict-ssl false && \
npm install --prod && \
ls -al
FROM base as python
WORKDIR /app
COPY docker-utils/GetTwitchDownloader.py .
RUN apt update && \
apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip && \
apt clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install PyGithub requests
RUN python GetTwitchDownloader.py
# Final image
FROM base
@@ -55,13 +64,14 @@ RUN npm install -g pm2 && \
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
apt clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install tdh-tcd pycryptodomex
RUN pip install pycryptodomex
WORKDIR /app
# User 1000 already exist from base image
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
RUN chown $UID:$GID .
RUN chmod +x /app/fix-scripts/*.sh
# Add some persistence data
#VOLUME ["/app/appdata"]

View File

@@ -162,6 +162,7 @@ app.use(bodyParser.json());
// use passport
app.use(auth_api.passport.initialize());
app.use(auth_api.passport.session());
// actual functions
@@ -2031,7 +2032,7 @@ app.post('/api/changeRolePermissions', optionalJwt, async (req, res) => {
// notifications
app.post('/api/getNotifications', optionalJwt, async (req, res) => {
const uuid = req.user.uid;
const uuid = req.isAuthenticated() ? req.user.uid : null;
const notifications = await db_api.getRecords('notifications', {user_uid: uuid});
@@ -2040,7 +2041,7 @@ app.post('/api/getNotifications', optionalJwt, async (req, res) => {
// set notifications to read
app.post('/api/setNotificationsToRead', optionalJwt, async (req, res) => {
const uuid = req.user.uid;
const uuid = req.isAuthenticated() ? req.user.uid : null;
const success = await db_api.updateRecords('notifications', {user_uid: uuid}, {read: true});
@@ -2048,7 +2049,7 @@ app.post('/api/setNotificationsToRead', optionalJwt, async (req, res) => {
});
app.post('/api/deleteNotification', optionalJwt, async (req, res) => {
const uid = req.body.uid;
const uid = req.isAuthenticated() ? req.user.uid : null;
const success = await db_api.removeRecord('notifications', {uid: uid});
@@ -2056,7 +2057,7 @@ app.post('/api/deleteNotification', optionalJwt, async (req, res) => {
});
app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => {
const uuid = req.user.uid;
const uuid = req.isAuthenticated() ? req.user.uid : null;
const success = await db_api.removeAllRecords('notifications', {user_uid: uuid});

View File

@@ -208,9 +208,6 @@ const DEFAULT_CONFIG = {
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_client_ID": "",
"twitch_client_secret": "",
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false,
"generate_NFO_files": false,

View File

@@ -110,18 +110,6 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_youtube_api_key',
'path': 'YoutubeDLMaterial.API.youtube_API_key'
},
'ytdl_use_twitch_api': {
'key': 'ytdl_use_twitch_api',
'path': 'YoutubeDLMaterial.API.use_twitch_API'
},
'ytdl_twitch_client_id': {
'key': 'ytdl_twitch_client_id',
'path': 'YoutubeDLMaterial.API.twitch_client_ID'
},
'ytdl_twitch_client_secret': {
'key': 'ytdl_twitch_client_secret',
'path': 'YoutubeDLMaterial.API.twitch_client_secret'
},
'ytdl_twitch_auto_download_chat': {
'key': 'ytdl_twitch_auto_download_chat',
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'

View File

@@ -698,9 +698,15 @@ exports.getRecords = async (table, filter_obj = null, return_count = false, sort
// Update
exports.updateRecord = async (table, filter_obj, update_obj) => {
exports.updateRecord = async (table, filter_obj, update_obj, nested_mode = false) => {
// local db override
if (using_local_db) {
if (nested_mode) {
// if object is nested we need to handle it differently
update_obj = utils.convertFlatObjectToNestedObject(update_obj);
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').merge(update_obj).write();
return true;
}
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write();
return true;
}
@@ -722,6 +728,18 @@ exports.updateRecords = async (table, filter_obj, update_obj) => {
return !!(output['result']['ok']);
}
exports.removePropertyFromRecord = async (table, filter_obj, remove_obj) => {
// local db override
if (using_local_db) {
const props_to_remove = Object.keys(remove_obj);
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').unset(props_to_remove).write();
return true;
}
const output = await database.collection(table).updateOne(filter_obj, {$unset: remove_obj});
return !!(output['result']['ok']);
}
exports.bulkUpdateRecordsByKey = async (table, key_label, update_obj) => {
// local db override
if (using_local_db) {

View File

@@ -245,11 +245,10 @@ async function collectInfo(download_uid) {
options.customOutput = category['custom_output'];
options.noRelativePath = true;
args = await exports.generateArgs(url, type, options, download['user_uid']);
args = utils.filterArgs(args, ['--no-simulate']);
info = await exports.getVideoInfoByURL(url, args, download_uid);
}
download['category'] = category;
const stripped_category = {name: category['name'], uid: category['uid']};
// setup info required to calculate download progress
@@ -272,6 +271,7 @@ async function collectInfo(download_uid) {
files_to_check_for_progress: files_to_check_for_progress,
expected_file_size: expected_file_size,
title: playlist_title ? playlist_title : info['title'],
category: stripped_category,
prefetched_info: null
});
}
@@ -350,7 +350,7 @@ async function downloadQueuedFile(download_uid) {
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
&& config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
let vodId = url.split('twitch.tv/videos/')[1];
vodId = vodId.split('?')[0];
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
@@ -552,7 +552,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
return new Promise(resolve => {
// remove bad args
const new_args = [...args];
const temp_args = utils.filterArgs(args, ['--no-simulate']);
const new_args = [...temp_args];
const archiveArgIndex = new_args.indexOf('--download-archive');
if (archiveArgIndex !== -1) {

View File

@@ -10,7 +10,7 @@ fi
# chown current working directory to current user
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
find . \! -user "$UID" -exec chown "$UID:$GID" '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
exec gosu "$UID:$GID" "$0" "$@"
fi

View File

@@ -51,43 +51,6 @@
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
},
"@sindresorhus/is": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz",
"integrity": "sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg=="
},
"@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"requires": {
"defer-to-connect": "^2.0.0"
}
},
"@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"requires": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
"@types/node": "*",
"@types/responselike": "^1.0.0"
}
},
"@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
},
"@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"requires": {
"@types/node": "*"
}
},
"@types/ldapjs": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.2.tgz",
@@ -795,19 +758,6 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
"integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="
},
"decompress-response": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-5.0.0.tgz",
"integrity": "sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw==",
"requires": {
"mimic-response": "^2.0.0"
}
},
"defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
},
"define-properties": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
@@ -911,99 +861,6 @@
"once": "^1.4.0"
}
},
"es-abstract": {
"version": "1.21.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
"integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==",
"requires": {
"array-buffer-byte-length": "^1.0.0",
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.2",
"es-set-tostringtag": "^2.0.1",
"es-to-primitive": "^1.2.1",
"function.prototype.name": "^1.1.5",
"get-intrinsic": "^1.2.0",
"get-symbol-description": "^1.0.0",
"globalthis": "^1.0.3",
"gopd": "^1.0.1",
"has": "^1.0.3",
"has-property-descriptors": "^1.0.0",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"internal-slot": "^1.0.5",
"is-array-buffer": "^3.0.2",
"is-callable": "^1.2.7",
"is-negative-zero": "^2.0.2",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
"is-string": "^1.0.7",
"is-typed-array": "^1.1.10",
"is-weakref": "^1.0.2",
"object-inspect": "^1.12.3",
"object-keys": "^1.1.1",
"object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.4.3",
"safe-regex-test": "^1.0.0",
"string.prototype.trim": "^1.2.7",
"string.prototype.trimend": "^1.0.6",
"string.prototype.trimstart": "^1.0.6",
"typed-array-length": "^1.0.4",
"unbox-primitive": "^1.0.2",
"which-typed-array": "^1.1.9"
},
"dependencies": {
"get-intrinsic": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.3"
}
}
}
},
"es-set-tostringtag": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
"integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
"requires": {
"get-intrinsic": "^1.1.3",
"has": "^1.0.3",
"has-tostringtag": "^1.0.0"
},
"dependencies": {
"get-intrinsic": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.3"
}
}
}
},
"es-shim-unscopables": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
"integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
"requires": {
"has": "^1.0.3"
}
},
"es-to-primitive": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
"is-symbol": "^1.0.2"
}
},
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -1029,11 +886,6 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"eventemitter3": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
},
"execa": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-3.2.0.tgz",
@@ -1122,6 +974,33 @@
}
}
},
"express-session": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
"requires": {
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"dependencies": {
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -1147,19 +1026,6 @@
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
"feed": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz",
"integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==",
"requires": {
"xml-js": "^1.6.11"
}
},
"file-type": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA=="
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -1301,22 +1167,6 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"function.prototype.name": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
"integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.19.0",
"functions-have-names": "^1.2.2"
}
},
"functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -1340,15 +1190,6 @@
"pump": "^3.0.0"
}
},
"get-symbol-description": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
"integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
"requires": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.1"
}
},
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
@@ -1468,11 +1309,6 @@
"function-bind": "^1.1.1"
}
},
"has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
"integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ=="
},
"has-property-descriptors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
@@ -1579,28 +1415,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"internal-slot": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
"integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==",
"requires": {
"get-intrinsic": "^1.2.0",
"has": "^1.0.3",
"side-channel": "^1.0.4"
},
"dependencies": {
"get-intrinsic": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.3"
}
}
}
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -1663,19 +1477,6 @@
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="
},
"is-date-object": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
"requires": {
"has-tostringtag": "^1.0.0"
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2420,6 +2221,31 @@
"xtend": "^4.0.0"
}
},
"multistream": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz",
"integrity": "sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==",
"requires": {
"inherits": "^2.0.1",
"readable-stream": "^2.0.5"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
}
}
},
"mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -2466,79 +2292,11 @@
"sorted-array-functions": "^1.3.0"
}
},
"node-telegram-bot-api": {
"version": "0.61.0",
"resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.61.0.tgz",
"integrity": "sha512-BZXd8Bh2C5+uBEQuuI3FD7TFJF3alV+6oFQt8CNLx3ldX/hsd+NYyllTX+Y+5X0tG+xtcRQQjbfLgz/4sRvmBQ==",
"requires": {
"array.prototype.findindex": "^2.0.2",
"bl": "^1.2.3",
"debug": "^3.2.7",
"eventemitter3": "^3.0.0",
"file-type": "^3.9.0",
"mime": "^1.6.0",
"pump": "^2.0.0",
"request": "^2.83.0",
"request-promise": "^4.2.2"
},
"dependencies": {
"bl": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz",
"integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==",
"requires": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
}
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
}
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
},
"normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
},
"npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -2623,19 +2381,6 @@
"require-at": "^1.0.6"
}
},
"p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
},
"p-event": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz",
"integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==",
"requires": {
"p-timeout": "^3.1.0"
}
},
"p-finally": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
@@ -2657,21 +2402,6 @@
"p-limit": "^3.0.2"
}
},
"p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"requires": {
"p-finally": "^1.0.0"
},
"dependencies": {
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="
}
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -2774,7 +2504,7 @@
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"performance-now": {
"version": "2.1.0",
@@ -2847,6 +2577,11 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw=="
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -2904,16 +2639,6 @@
"minimatch": "^3.0.4"
}
},
"regexp.prototype.flags": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
"integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"functions-have-names": "^1.2.2"
}
},
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
@@ -2989,14 +2714,6 @@
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"requires": {
"lowercase-keys": "^2.0.0"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@@ -3066,11 +2783,6 @@
"sparse-bitfield": "^3.0.3"
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -3426,11 +3138,6 @@
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
"type-fest": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.10.0.tgz",
"integrity": "sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -3466,6 +3173,14 @@
"which-boxed-primitive": "^1.0.2"
}
},
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
@@ -3667,14 +3382,6 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"requires": {
"sax": "^1.2.4"
}
},
"xmlbuilder2": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.0.2.tgz",

View File

@@ -27,6 +27,7 @@
"compression": "^1.7.4",
"config": "^3.2.3",
"express": "^4.17.3",
"express-session": "^1.17.3",
"feed": "^4.2.2",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0",

View File

@@ -101,7 +101,7 @@ exports.setupTasks = async () => {
const tasks_keys = Object.keys(TASKS);
for (let i = 0; i < tasks_keys.length; i++) {
const task_key = tasks_keys[i];
const mergedDefaultOptions = Object.assign(defaultOptions['all'], defaultOptions[task_key] || {});
const mergedDefaultOptions = Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {});
const task_in_db = await db_api.getRecord('tasks', {key: task_key});
if (!task_in_db) {
// insert task metadata into table if missing, eventually move title to UI
@@ -115,14 +115,16 @@ exports.setupTasks = async () => {
data: null,
error: null,
schedule: null,
options: Object.assign(defaultOptions['all'], defaultOptions[task_key] || {})
options: Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {})
});
} else {
// verify all options exist in task
for (const key of Object.keys(mergedDefaultOptions)) {
const option_key = `options.${key}`;
// Remove any potential mangled option keys (#861)
await db_api.removePropertyFromRecord('tasks', {key: task_key}, {[option_key]: true});
if (!(task_in_db.options && task_in_db.options.hasOwnProperty(key))) {
const option_key = `options.${key}`
await db_api.updateRecord('tasks', {key: task_key}, {[option_key]: mergedDefaultOptions[key]});
await db_api.updateRecord('tasks', {key: task_key}, {[option_key]: mergedDefaultOptions[key]}, true);
}
}

View File

@@ -175,6 +175,15 @@ describe('Database', async function() {
await db_api.removeRecord('test', {test_update: 'test'});
});
it('Remove property from record', async function() {
await db_api.insertRecordIntoTable('test', {test_keep: 'test', test_remove: 'test'});
await db_api.removePropertyFromRecord('test', {test_keep: 'test'}, {test_remove: true});
const updated_record = await db_api.getRecord('test', {test_keep: 'test'});
assert(updated_record['test_keep']);
assert(!updated_record['test_remove']);
await db_api.removeRecord('test', {test_keep: 'test'});
});
it('Remove record', async function() {
await db_api.insertRecordIntoTable('test', {test_remove: 'test'});
const delete_succeeded = await db_api.removeRecord('test', {test_remove: 'test'});
@@ -499,7 +508,7 @@ describe('Downloader', function() {
});
describe('Twitch', async function () {
const twitch_api = require('../twitch');
const example_vod = '1493770675';
const example_vod = '1710641401';
it('Download VOD', async function() {
const sample_path = path.join('test', 'sample.twitch_chat.json');
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
@@ -699,4 +708,24 @@ describe('Utils', async function() {
const stripped_obj = utils.stripPropertiesFromObject(test_obj, ['test1', 'test3']);
assert(!stripped_obj['test1'] && stripped_obj['test2'] && !stripped_obj['test3'])
});
it('Convert flat object to nested object', async function() {
// No modfication
const flat_obj0 = {'test1': {'test_sub': true}, 'test2': {test_sub: true}};
const nested_obj0 = utils.convertFlatObjectToNestedObject(flat_obj0);
assert(nested_obj0['test1'] && nested_obj0['test1']['test_sub']);
assert(nested_obj0['test2'] && nested_obj0['test2']['test_sub']);
// Standard setup
const flat_obj1 = {'test1.test_sub': true, 'test2.test_sub': true};
const nested_obj1 = utils.convertFlatObjectToNestedObject(flat_obj1);
assert(nested_obj1['test1'] && nested_obj1['test1']['test_sub']);
assert(nested_obj1['test2'] && nested_obj1['test2']['test_sub']);
// Nested branches
const flat_obj2 = {'test1.test_sub': true, 'test1.test2.test_sub': true};
const nested_obj2 = utils.convertFlatObjectToNestedObject(flat_obj2);
assert(nested_obj2['test1'] && nested_obj2['test1']['test_sub']);
assert(nested_obj2['test1'] && nested_obj2['test1']['test2'] && nested_obj2['test1']['test2']['test_sub']);
});
});

View File

@@ -4,19 +4,28 @@ const logger = require('./logger');
const moment = require('moment');
const fs = require('fs-extra')
const path = require('path');
const { promisify } = require('util');
const child_process = require('child_process');
async function getCommentsForVOD(clientID, clientSecret, vodId) {
const { promisify } = require('util');
const child_process = require('child_process');
async function getCommentsForVOD(vodId) {
const exec = promisify(child_process.exec);
// Reject invalid params to prevent command injection attack
if (!clientID.match(/^[0-9a-z]+$/) || !clientSecret.match(/^[0-9a-z]+$/) || !vodId.match(/^[0-9a-z]+$/)) {
logger.error('Client ID, client secret, and VOD ID must be purely alphanumeric. Twitch chat download failed!');
if (!vodId.match(/^[0-9a-z]+$/)) {
logger.error('VOD ID must be purely alphanumeric. Twitch chat download failed!');
return null;
}
const result = await exec(`tcd --video ${vodId} --client-id ${clientID} --client-secret ${clientSecret} --format json -o appdata`, {stdio:[0,1,2]});
const is_windows = process.platform === 'win32';
const cliExt = is_windows ? '.exe' : ''
const cliPath = `TwitchDownloaderCLI${cliExt}`
if (!fs.existsSync(cliPath)) {
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
return null;
}
const result = await exec(`TwitchDownloaderCLI chatdownload -u ${vodId} -o appdata/${vodId}.json`, {stdio:[0,1,2]});
if (result['stderr']) {
logger.error(`Failed to download twitch comments for ${vodId}`);
@@ -73,9 +82,7 @@ async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
const chat = await getCommentsForVOD(twitch_client_id, twitch_client_secret, vodId);
const chat = await getCommentsForVOD(vodId);
// save file if needed params are included
let file_path = null;

View File

@@ -501,6 +501,23 @@ exports.updateLoggerLevel = (new_logger_level) => {
logger.transports[2].level = new_logger_level;
}
exports.convertFlatObjectToNestedObject = (obj) => {
const result = {};
for (const key in obj) {
const nestedKeys = key.split('.');
let currentObj = result;
for (let i = 0; i < nestedKeys.length; i++) {
if (i === nestedKeys.length - 1) {
currentObj[nestedKeys[i]] = obj[key];
} else {
currentObj[nestedKeys[i]] = currentObj[nestedKeys[i]] || {};
currentObj = currentObj[nestedKeys[i]];
}
}
}
return result;
}
// objects
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {

View File

@@ -0,0 +1,53 @@
import platform
import requests
import shutil
import os
import re
from github import Github
machine = platform.machine()
def isARM():
return True if machine.startswith('arm') else False
def getLatestFileInRepo(repo, search_string):
# Create an unauthenticated instance of the Github object
g = Github(os.environ.get('GH_TOKEN'))
# Replace with the repository owner and name
repo = g.get_repo(repo)
# Get all releases of the repository
releases = repo.get_releases()
# Loop through the releases in reverse order (from latest to oldest)
for release in list(releases):
# Get the release assets (files attached to the release)
assets = release.get_assets()
# Loop through the assets
for asset in assets:
if re.search(search_string, asset.name):
print(f'Downloading: {asset.name}')
response = requests.get(asset.browser_download_url)
with open(asset.name, 'wb') as f:
f.write(response.content)
print(f'Download complete: {asset.name}. Unzipping...')
shutil.unpack_archive(asset.name, './')
print(f'Unzipping complete!')
os.remove(asset.name)
break
else:
continue
break
else:
# If no matching release is found, print a message
print(f'No release found with {search_string}')
def getLatestCLIRelease():
isArm = isARM()
searchString = r'.*CLI.*' + "LinuxArm.zip" if isArm else "Linux-x64.zip"
getLatestFileInRepo("lay295/TwitchDownloader", searchString)
getLatestCLIRelease()

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.3.0",
"version": "4.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -2,7 +2,7 @@
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''" [loading]="false"></app-unified-file-card>
</div>
</div>
</div>

View File

@@ -31,9 +31,11 @@
<mat-form-field class="value-input">
<input matInput [(ngModel)]="rule['value']">
</mat-form-field>
<span class="rule-buttons">
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
</span>
</mat-list-item>
</mat-list>

View File

@@ -1,5 +1,5 @@
.operator-select {
width: 55px;
width: 90px;
}
.property-select {
@@ -14,3 +14,16 @@
.value-input {
margin-left: 10px;
}
:host ::ng-deep.mdc-list-item {
height: 75px !important;
}
:host ::ng-deep.mdc-list-item__content {
pointer-events: unset;
}
.rule-buttons {
position: relative;
top: 8px;
}

View File

@@ -37,7 +37,8 @@
<input [(ngModel)]="new_file.thumbnailURL" matInput [disabled]="!editing || new_file.thumbnailPath">
</mat-form-field>
<mat-form-field *ngIf="initialized && postsService.categories" class="info-field">
<mat-select placeholder="Category" i18n-placeholder="Category" [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-label i18n="Category">Category</mat-label>
<mat-select [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-option [value]="{}">
N/A
</mat-option>

View File

@@ -38,7 +38,7 @@
</ng-container>
<ng-container *ngIf="db_file || playlist[currentIndex]"></ng-container>
<button (click)="openFileInfoDialog()" *ngIf="db_file || db_playlist" mat-icon-button><mat-icon>info</mat-icon></button>
<button *ngIf="db_file && db_file.url.includes('twitch.tv') && postsService['config']['API']['use_twitch_API']" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
<button *ngIf="db_file && db_file.url.includes('twitch.tv')" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
</div>
</div>
</div>
@@ -51,7 +51,7 @@
<app-concurrent-stream *ngIf="db_file && api && postsService.config" (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists'] && postsService['config']['API']['use_twitch_API']">
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists']">
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv')">
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
</ng-container>

View File

@@ -269,25 +269,9 @@
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_twitch_API']"><ng-container i18n="Use Twitch API setting">Use Twitch API</ng-container></mat-checkbox>
</div>
<div *ngIf="new_config['API']['use_twitch_API']" class="col-12 mt-1">
<div class="col-12 mt-1">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="Twitch Client ID">Twitch Client ID</mat-label>
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_ID']" matInput required>
<mat-hint><a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch Client ID setting hint">Generating an ID/secret is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="Twitch Client Secret">Twitch Client Secret</mat-label>
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_secret']" matInput required>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1432,7 +1432,7 @@
</trans-unit>
<trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
<source>Profile</source>
<target>个人资料</target>
<target state="translated">资料</target>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">19</context>
@@ -3033,7 +3033,7 @@
</trans-unit>
<trans-unit id="4e1fdb6039c7c6b7630ed70d6d20eb0c9db7d342" datatype="html">
<source>Video only</source>
<target state="translated">视频</target>
<target state="translated">视频</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">55</context>
@@ -4011,8 +4011,8 @@
</context-group>
</trans-unit>
<trans-unit id="39921032161993566" datatype="html">
<source>Playlist created.</source>
<target state="translated">创建播放列表</target>
<source>Successfully created playlist!</source>
<target state="translated">成功创建播放列表</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">56</context>
@@ -4191,6 +4191,830 @@
<context context-type="linenumber">299</context>
</context-group>
</trans-unit>
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html">
<source>Archives</source>
<target state="translated">存档</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Archives menu label</note>
</trans-unit>
<trans-unit id="5ca707824ab93066c7d9b44e1b8bf216725c2c22" datatype="html">
<source>Filter</source>
<target state="translated">筛选</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
<note priority="1" from="description">Filter</note>
</trans-unit>
<trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html">
<source>ID</source>
<target state="translated">ID</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
<note priority="1" from="description">ID</note>
</trans-unit>
<trans-unit id="28da11220a3377ddce3c7948825d33101f142782" datatype="html">
<source>Extractor</source>
<target state="translated">提取</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">57</context>
</context-group>
<note priority="1" from="description">Extractor</note>
</trans-unit>
<trans-unit id="c150a30bbbdb175b4d08820196a9acb66b167653" datatype="html">
<source>Archives empty</source>
<target state="translated">存档为空</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">72</context>
</context-group>
<note priority="1" from="description">Archives empty</note>
</trans-unit>
<trans-unit id="51a161ce175abcd44f6c6cbd0e996681bf553ac3" datatype="html">
<source>Delete selected</source>
<target state="translated">删除所选内容</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">77</context>
</context-group>
<note priority="1" from="description">Delete selected</note>
</trans-unit>
<trans-unit id="c41475a25c9f9d9639db9efa56637603a77528b4" datatype="html">
<source>Download archive</source>
<target state="translated">下载存档</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">80</context>
</context-group>
<note priority="1" from="description">Download archive</note>
</trans-unit>
<trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html">
<source>None</source>
<target state="translated">无</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">84</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">126</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">36</context>
</context-group>
<note priority="1" from="description">None</note>
</trans-unit>
<trans-unit id="4b3972c3e9485218508a95f7a4ce7758e3f09ced" datatype="html">
<source>Upload</source>
<target state="translated">上传</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">137</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">30</context>
</context-group>
<note priority="1" from="description">Upload</note>
</trans-unit>
<trans-unit id="6549265851868599441" datatype="html">
<source>Video</source>
<target state="translated">视频</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="3159807825117518005" datatype="html">
<source>Delete archives</source>
<target state="translated">删除存档</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">152</context>
</context-group>
</trans-unit>
<trans-unit id="8425787787095143143" datatype="html">
<source>Would you like to delete <x id="selected archives amount" equiv-text="this.selection.selected.length"/> archive(s)?</source>
<target state="translated">是否要删除 <x id="selected archives amount" equiv-text="this.selection.selected.length"/> 存档?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">153</context>
</context-group>
</trans-unit>
<trans-unit id="8224301330941792118" datatype="html">
<source>Failed to delete archive items!</source>
<target state="translated">无法删除存档项目!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="019d4bd6a5690f0cfa0ecf346a4e6bf7f0d8debb" datatype="html">
<source>Remove</source>
<target state="translated">移除</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<note priority="1" from="description">Remove</note>
</trans-unit>
<trans-unit id="6219551536751479443" datatype="html">
<source>Finished downloading</source>
<target state="translated">下载完成</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="5947241266456580665" datatype="html">
<source>Download failed</source>
<target state="translated">下载失败</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="8443034725057696949" datatype="html">
<source>Task finished</source>
<target state="translated">任务完成</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="8564202903947049539" datatype="html">
<source>Play</source>
<target state="translated">播放</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="8643601595923420698" datatype="html">
<source>Retry download</source>
<target state="translated">重试下载</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="8571838164752006148" datatype="html">
<source>View error</source>
<target state="translated">查看错误</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="5709555629190115111" datatype="html">
<source>View task</source>
<target state="translated">查看任务</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="5a105e7bd7e7db6ea211fe950fc9f317379acceb" datatype="html">
<source>No notifications available</source>
<target state="translated">没有通知</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<note priority="1" from="description">No notifications available</note>
</trans-unit>
<trans-unit id="6876310993601590130" datatype="html">
<source>Download completed</source>
<target state="translated">下载完成</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="1879058637439215882" datatype="html">
<source>Download error</source>
<target state="translated">下载错误</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="4578192247039196794" datatype="html">
<source>Task</source>
<target state="translated">任务</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="5000203534763292992" datatype="html">
<source>Download restarted!</source>
<target state="translated">下载已重新启动!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="7911845622864460134" datatype="html">
<source>Video only</source>
<target state="translated">仅视频</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">55</context>
</context-group>
</trans-unit>
<trans-unit id="6437411876967154040" datatype="html">
<source>Audio only</source>
<target state="translated">仅音频</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">60</context>
</context-group>
</trans-unit>
<trans-unit id="4665451070906079743" datatype="html">
<source>Favorited</source>
<target state="translated">收藏</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">65</context>
</context-group>
</trans-unit>
<trans-unit id="3533826530554274875" datatype="html">
<source>Upload Date</source>
<target state="translated">上传日期</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="6268070779441507380" datatype="html">
<source>Download Date</source>
<target state="translated">下载日期</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html">
<source>Name</source>
<target state="translated">名称</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="c65dd978b3c7566551c0ebefb234c2d41942b847" datatype="html">
<source>Delete files older than</source>
<target state="translated">删除早于</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">6</context>
</context-group>
<note priority="1" from="description">Delete files older than</note>
</trans-unit>
<trans-unit id="56b1a3c93fb95fed1805005c561a5e431d57ffae" datatype="html">
<source>Blacklist all files</source>
<target state="translated">将所有文件列入黑名单</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<note priority="1" from="description">Blacklist deleted files</note>
</trans-unit>
<trans-unit id="9aa1b4779a515170b297d2c0507e6ff9d2e3e0e0" datatype="html">
<source>Blacklist deleted subscription files</source>
<target state="translated">黑名单删除的订阅文件</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
<note priority="1" from="description">Blacklist deleted subscription files</note>
</trans-unit>
<trans-unit id="cdf5297d8d080a78e8b10debc5c38b7845a3cbe7" datatype="html">
<source>Do not ask for confirmation</source>
<target state="translated">不要求确认</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
<note priority="1" from="description">Do not ask for confirmation</note>
</trans-unit>
<trans-unit id="7b4585a9072f3c1292972c14a3d0e14978fbfc9c" datatype="html">
<source>Delete old files:</source>
<target state="translated">删除旧文件:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.html</context>
<context context-type="linenumber">66</context>
</context-group>
<note priority="1" from="description">Delete old files</note>
</trans-unit>
<trans-unit id="9176960997786930103" datatype="html">
<source>Error for: <x id="PH" equiv-text="task['title']"/></source>
<target state="translated">错误: <x id="PH" equiv-text="task['title']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="5ac5a0e5ffe8e5623b40696f4c2403c17349271f" datatype="html">
<source>Sidepanel mode</source>
<target state="translated">侧板模式</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
<note priority="1" from="description">Sidepanel mode</note>
</trans-unit>
<trans-unit id="1f2809e6a99d511fdb6eaf041d785fe54d0680cc" datatype="html">
<source>File card size</source>
<target state="translated">文件卡大小</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">54</context>
</context-group>
<note priority="1" from="description">File card size</note>
</trans-unit>
<trans-unit id="d618f383a0ea2458eeb945a85190d4a002ea394b" datatype="html">
<source>Arg</source>
<target state="translated">参数</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html</context>
<context context-type="linenumber">41</context>
</context-group>
<note priority="1" from="description">Arg</note>
</trans-unit>
<trans-unit id="c35ef0f03a863d33b04aae6807f140397a50f491" datatype="html">
<source>Generate RSS URL</source>
<target state="translated">生成 RSS 网址</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">306</context>
</context-group>
<note priority="1" from="description">Generate RSS URL</note>
</trans-unit>
<trans-unit id="c2faa86201eab08b5b39b5437f96ab9432e125e7" datatype="html">
<source>Item limit</source>
<target state="translated">项目限制</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">46</context>
</context-group>
<note priority="1" from="description">Item limit</note>
</trans-unit>
<trans-unit id="b78a98bc54259a29cf6250dbaeab5fe11fae91cf" datatype="html">
<source>Favorited</source>
<target state="translated">收藏</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">51</context>
</context-group>
<note priority="1" from="description">Favorited</note>
</trans-unit>
<trans-unit id="8336047719608684263" datatype="html">
<source>Unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/></source>
<target state="translated">取消订阅 <x id="subscription name" equiv-text="this.sub['name']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="784837056777689544" datatype="html">
<source>Would you like to unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/>?</source>
<target state="translated">是否要取消订阅 <x id="subscription name" equiv-text="this.sub['name']"/>?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="1698114086921246480" datatype="html">
<source>Unsubscribe</source>
<target state="translated">取消订阅</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="1091872159779006651" datatype="html">
<source>You must input a time!</source>
<target state="translated">你必须输入一个时间!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.ts</context>
<context context-type="linenumber">70</context>
</context-group>
</trans-unit>
<trans-unit id="d57c023a4cf63b2f12c10328c15b636ff18929aa" datatype="html">
<source>Best</source>
<target state="translated">最佳</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.html</context>
<context context-type="linenumber">24,25</context>
</context-group>
<note priority="1" from="description">Best</note>
</trans-unit>
<trans-unit id="35cf4cdcedc8ef3f94b6100e0d86836e31dbb908" datatype="html">
<source>Force autoplay</source>
<target state="translated">强制自动播放</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">235</context>
</context-group>
<note priority="1" from="description">Force autoplay setting</note>
</trans-unit>
<trans-unit id="37469c9f3e31d95087fa22b6c9c3bc64adf1692d" datatype="html">
<source>Enable RSS Feed</source>
<target state="translated">启用 RSS 订阅</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">304</context>
</context-group>
<note priority="1" from="description">Enable RSS Feed setting</note>
</trans-unit>
<trans-unit id="61d6b5fa4311b1c617b66dad72496f9dd43b07b4" datatype="html">
<source>Be careful enabling this with multi-user mode! User data may be exposed.</source>
<target state="translated">使用多用户模式启用此功能时要小心!用户数据可能会暴露。</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">305</context>
</context-group>
<note priority="1" from="description">RSS Feed prefix</note>
</trans-unit>
<trans-unit id="fd467148a18f0921c10d116d4e0174fe29452be4" datatype="html">
<source>See documentation here.</source>
<target state="translated">请看这里的文档。</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">307</context>
</context-group>
<note priority="1" from="description">RSS feed documentation</note>
</trans-unit>
<trans-unit id="8bcabdf6b16cad0313a86c7e940c5e3ad7f9f8ab" datatype="html">
<source>Notifications</source>
<target state="translated">通知</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">376</context>
</context-group>
<note priority="1" from="description">Notifications settings label</note>
</trans-unit>
<trans-unit id="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html">
<source>Download error</source>
<target state="translated">下载错误</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">392</context>
</context-group>
<note priority="1" from="description">Download error</note>
</trans-unit>
<trans-unit id="c40370dc182b5e4333828b70f7478bde58bb5cfe" datatype="html">
<source>Enable notifications</source>
<target state="translated">启用通知</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">382</context>
</context-group>
<note priority="1" from="description">Enable notifications setting</note>
</trans-unit>
<trans-unit id="33a7c6d5ff3515fa237f1fd4e43df8b65373954d" datatype="html">
<source>Enable all notifications</source>
<target state="translated">启用所有通知</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">385</context>
</context-group>
<note priority="1" from="description">Enable all notifications setting</note>
</trans-unit>
<trans-unit id="9c562d26e041390ecc3f49dabc51cc50ebba7469" datatype="html">
<source>Allowed notification types</source>
<target state="translated">允许的通知类型</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">389</context>
</context-group>
<note priority="1" from="description">Allowed notification types</note>
</trans-unit>
<trans-unit id="c5dc5fbcce45e9b1530e2a5c2baa8ebe722aef4c" datatype="html">
<source>Download complete</source>
<target state="translated">下载完成</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">391</context>
</context-group>
<note priority="1" from="description">Download complete</note>
</trans-unit>
<trans-unit id="2361a4f76caaa4574803fbcdca8b0a47c91cc7ed" datatype="html">
<source>Task finished</source>
<target state="translated">任务完成</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">393</context>
</context-group>
<note priority="1" from="description">Task finished</note>
</trans-unit>
<trans-unit id="9e766e11a9de375907aaf566897ecc6dac393ebc" datatype="html">
<source>Webhook URL</source>
<target state="translated">Webhook 网址</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">399</context>
</context-group>
<note priority="1" from="description">webhook URL</note>
</trans-unit>
<trans-unit id="8c1bf02206fbc371ff69ab1b7e35a17ba29d169d" datatype="html">
<source>Use ntfy API</source>
<target state="translated">使用 ntfy API</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">405</context>
</context-group>
<note priority="1" from="description">Use ntfy API setting</note>
</trans-unit>
<trans-unit id="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html">
<source>See docs here.</source>
<target state="translated">请看这里的文档。</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">411</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">421</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">428</context>
</context-group>
<note priority="1" from="description">ntfy API setting hint</note>
</trans-unit>
<trans-unit id="55f559d6f666b945479f534b0c182f70cd0a8a69" datatype="html">
<source>Gotify server URL</source>
<target state="translated">Gotify 服务网址</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">419</context>
</context-group>
<note priority="1" from="description">Gotify server URL</note>
</trans-unit>
<trans-unit id="b770c48628d98cb4633d6a17e3f0ba0265376af5" datatype="html">
<source>Gotify app token</source>
<target state="translated">Gotify 应用令牌</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">426</context>
</context-group>
<note priority="1" from="description">Gotify app token</note>
</trans-unit>
<trans-unit id="38992954440d6afb54aeb58af12ca0123ee5e26e" datatype="html">
<source>Use Telegram API</source>
<target state="translated">使用 Telegram API</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">432</context>
</context-group>
<note priority="1" from="description">Use Telegram API setting</note>
</trans-unit>
<trans-unit id="2e076ff9866213d0815961c494aa48b177046b9d" datatype="html">
<source>Telegram bot token</source>
<target state="translated">Telegram 机器人令牌</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">436</context>
</context-group>
<note priority="1" from="description">Telegram bot token</note>
</trans-unit>
<trans-unit id="eeb0ba2e4743901d8f5eebd9a3529aa1f236c608" datatype="html">
<source>Create bot here.</source>
<target state="translated">在此处创建机器人。</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">438</context>
</context-group>
<note priority="1" from="description">Telegram bot create link</note>
</trans-unit>
<trans-unit id="144e1a21ebe8fa238f88d2ac27515ed711cfc9a0" datatype="html">
<source>Telegram chat ID</source>
<target state="translated">Telegram 聊天 ID</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">443</context>
</context-group>
<note priority="1" from="description">Telegram chat ID</note>
</trans-unit>
<trans-unit id="3e420c675b8f3f3702576d52e8bb6e8e1d3feda0" datatype="html">
<source>How do I get the chat ID?</source>
<target state="translated">如何获取聊天 ID</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">445</context>
</context-group>
<note priority="1" from="description">Telegram chat ID help</note>
</trans-unit>
<trans-unit id="a4ed8eba1e057e67d5c2d87b52230f182b3dae4e" datatype="html">
<source>Restart required.</source>
<target state="translated">需要重新启动。</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">465</context>
</context-group>
<note priority="1" from="description">Restart required hint</note>
</trans-unit>
<trans-unit id="6785427850041119037" datatype="html">
<source>Delete category</source>
<target state="translated">删除类别</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">173</context>
</context-group>
</trans-unit>
<trans-unit id="2481374649045841364" datatype="html">
<source>Would you like to delete <x id="category name" equiv-text="category['name']"/>?</source>
<target state="translated">您要删除 <x id="category name" equiv-text="category['name']"/>?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="7332320960988475089" datatype="html">
<source>Successfully deleted <x id="category name" equiv-text="category['name']"/>!</source>
<target state="translated">已成功删除 <x id="category name" equiv-text="category['name']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">183</context>
</context-group>
</trans-unit>
<trans-unit id="3371159074051387771" datatype="html">
<source>Failed to delete <x id="category name" equiv-text="category['name']"/>!</source>
<target state="translated">删除 <x id="category name" equiv-text="category['name']"/> 失败!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">187</context>
</context-group>
</trans-unit>
<trans-unit id="8c6e24eab969d9f63a8a0e9d617aee3b99e28ae6" datatype="html">
<source>Play all</source>
<target state="translated">全部播放</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
<context context-type="linenumber">17</context>
</context-group>
<note priority="1" from="description">Play all</note>
</trans-unit>
<trans-unit id="674a999dd48d7da565ffdd105602261b8a4761ea" datatype="html">
<source>Download zip</source>
<target state="translated">下载压缩包 (ZIP)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
<note priority="1" from="description">Download zip</note>
</trans-unit>
<trans-unit id="0ed98b4c6ec1db6708a963e8a2699478ac97f55c" datatype="html">
<source>Add subscription</source>
<target state="translated">添加订阅</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscriptions/subscriptions.component.html</context>
<context context-type="linenumber">60</context>
</context-group>
<note priority="1" from="description">Add subscription</note>
</trans-unit>
<trans-unit id="8953483585652369683" datatype="html">
<source>Archive successfully imported!</source>
<target state="translated">存档成功导入!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit id="ea2b65121b93921fe54692025da9b9e3ce779ad5" datatype="html">
<source>Task settings - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></source>
<target state="translated">任务设置 - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<note priority="1" from="description">Task settings</note>
</trans-unit>
<trans-unit id="06f503e492d6dbcf59e7b9c412ca86913d718689" datatype="html">
<source>ntfy topic URL</source>
<target state="translated">ntfy 话题网址</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">409</context>
</context-group>
<note priority="1" from="description">ntfy topic URL</note>
</trans-unit>
<trans-unit id="347407180135731058" datatype="html">
<source>Audio</source>
<target state="translated">音频</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">44</context>
</context-group>
</trans-unit>
<trans-unit id="7022070615528435141" datatype="html">
<source>Delete</source>
<target state="translated">删除</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">154</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">175</context>
</context-group>
</trans-unit>
<trans-unit id="2525880134753073592" datatype="html">
<source>Successfully deleted archive items!</source>
<target state="translated">已成功删除存档项目!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">172</context>
</context-group>
</trans-unit>
<trans-unit id="2492098975665776610" datatype="html">
<source>File Size</source>
<target state="translated">文件大小</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="7410432243549869948" datatype="html">
<source>Duration</source>
<target state="translated">期间</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">29</context>
</context-group>
</trans-unit>
<trans-unit id="11a0771f88158a540a54e0e4ec5d25733d65fc0e" datatype="html">
<source>Favorite</source>
<target state="translated">喜欢</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Favorite button</note>
</trans-unit>
<trans-unit id="1d4fa01d25990f60abf21c3a451fa8ba262b7912" datatype="html">
<source>Unfavorite</source>
<target state="translated">不喜欢</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
<note priority="1" from="description">Unfavorite button</note>
</trans-unit>
<trans-unit id="1a9b415816364f554ee411020e65219092655271" datatype="html">
<source>Title filter</source>
<target state="translated">标题过滤</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
<note priority="1" from="description">Title filter</note>
</trans-unit>
<trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
<source>User</source>
<target state="translated">用户</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">25</context>
</context-group>
<note priority="1" from="description">User</note>
</trans-unit>
<trans-unit id="9aa62bf1a535a97a4d752bbc5cf1c31af0f0c1f7" datatype="html">
<source>Supports regex</source>
<target state="translated">支持正则表达式</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">10</context>
</context-group>
<note priority="1" from="description">Supports regex</note>
</trans-unit>
<trans-unit id="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html">
<source>Use gotify API</source>
<target state="translated">使用 gotify API</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">415</context>
</context-group>
<note priority="1" from="description">Use gotify API setting</note>
</trans-unit>
</body>
</file>
</xliff>