add the ability to reload 'YTDL_OPTIONS' when file is modified

This commit is contained in:
xerdream 2025-07-21 11:20:53 +08:00
parent ca0aac4051
commit afbf8b07d6
7 changed files with 215 additions and 2 deletions

View File

@ -12,6 +12,7 @@ python-socketio = "~=5.0"
yt-dlp = "*"
mutagen = "*"
curl-cffi = "*"
watchfiles = "*"
[requires]
python_version = "3.13"

131
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "0059b3e28e6bdffb0cb683014be12e747d67ba50c78953a1f49d1418e877807b"
"sha256": "b987a00631108daa5b36fc6f461bd285b5817f524ac986ae57d3d12744685a38"
},
"pipfile-spec": 6,
"requires": {
@ -125,6 +125,14 @@
"markers": "python_version >= '3.9'",
"version": "==1.3.2"
},
"anyio": {
"hashes": [
"sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028",
"sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"
],
"markers": "python_version >= '3.9'",
"version": "==4.9.0"
},
"attrs": {
"hashes": [
"sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3",
@ -621,6 +629,127 @@
"markers": "python_version >= '3.6'",
"version": "==1.1.0"
},
"sniffio": {
"hashes": [
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
"sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
],
"markers": "python_version >= '3.7'",
"version": "==1.3.1"
},
"watchfiles": {
"hashes": [
"sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a",
"sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f",
"sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6",
"sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3",
"sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7",
"sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a",
"sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259",
"sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297",
"sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1",
"sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c",
"sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a",
"sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b",
"sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb",
"sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc",
"sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b",
"sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339",
"sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9",
"sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df",
"sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb",
"sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4",
"sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5",
"sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc",
"sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c",
"sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8",
"sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433",
"sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12",
"sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30",
"sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0",
"sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86",
"sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c",
"sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5",
"sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866",
"sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb",
"sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2",
"sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e",
"sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575",
"sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f",
"sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a",
"sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f",
"sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d",
"sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277",
"sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9",
"sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf",
"sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92",
"sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72",
"sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b",
"sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68",
"sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa",
"sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc",
"sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b",
"sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd",
"sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4",
"sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7",
"sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792",
"sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9",
"sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0",
"sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297",
"sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef",
"sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179",
"sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d",
"sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea",
"sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5",
"sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee",
"sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82",
"sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011",
"sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e",
"sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4",
"sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf",
"sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db",
"sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20",
"sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4",
"sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575",
"sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa",
"sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c",
"sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f",
"sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f",
"sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267",
"sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018",
"sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2",
"sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d",
"sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd",
"sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47",
"sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb",
"sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29",
"sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147",
"sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8",
"sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670",
"sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587",
"sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97",
"sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c",
"sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5",
"sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e",
"sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e",
"sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6",
"sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc",
"sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e",
"sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8",
"sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895",
"sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7",
"sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432",
"sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc",
"sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633",
"sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f",
"sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77",
"sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12",
"sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==1.1.0"
},
"wsproto": {
"hashes": [
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",

View File

@ -61,6 +61,7 @@ Certain values can be set via environment variables, using the `-e` parameter on
* __DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT__: Maximum number of playlist items that can be downloaded. Defaults to `0` (no limit).
* __YTDL_OPTIONS__: Additional options to pass to yt-dlp, in JSON format. [See available options here](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L220). They roughly correspond to command-line options, though some do not have exact equivalents here, for example `--recode-video` has to be specified via `postprocessors`. Also note that dashes are replaced with underscores. You may find [this script](https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py) helpful for converting from command line options to `YTDL_OPTIONS`.
* __YTDL_OPTIONS_FILE__: A path to a JSON file that will be loaded and used for populating `YTDL_OPTIONS` above. Please note that if both `YTDL_OPTIONS_FILE` and `YTDL_OPTIONS` are specified, the options in `YTDL_OPTIONS` take precedence.
* __YTDL_OPTIONS_FILE_RELOAD__:Reload `YTDL_OPTIONS` when file is modified. Defaults to `false`.
* __ROBOTS_TXT__: A path to a `robots.txt` file mounted in the container
* __DOWNLOAD_MODE__ :This flag controls how downloads are scheduled and executed. Options are `sequential`, `concurrent`, and `limited`. Defaults to `limited`:
* `sequential`: Downloads are processed one at a time. A new download wont start until the previous one has finished. This mode is useful for conserving system resources or ensuring downloads occur in a strict order.

View File

@ -3,6 +3,8 @@
import os
import sys
import asyncio
from pathlib import Path
from aiohttp import web
from aiohttp.log import access_logger
import ssl
@ -12,6 +14,7 @@ import logging
import json
import pathlib
import re
from watchfiles import DefaultFilter, Change, awatch
from ytdl import DownloadQueueNotifier, DownloadQueue
from yt_dlp.version import __version__ as yt_dlp_version
@ -39,6 +42,7 @@ class Config:
'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT' : '0',
'YTDL_OPTIONS': '{}',
'YTDL_OPTIONS_FILE': '',
'YTDL_OPTIONS_FILE_RELOAD': 'false',
'ROBOTS_TXT': '',
'HOST': '0.0.0.0',
'PORT': '8081',
@ -53,7 +57,7 @@ class Config:
'ENABLE_ACCESSLOG': 'false',
}
_BOOLEAN = ('DOWNLOAD_DIRS_INDEXABLE', 'CUSTOM_DIRS', 'CREATE_CUSTOM_DIRS', 'DELETE_FILE_ON_TRASHCAN', 'DEFAULT_OPTION_PLAYLIST_STRICT_MODE', 'HTTPS', 'ENABLE_ACCESSLOG')
_BOOLEAN = ('DOWNLOAD_DIRS_INDEXABLE', 'CUSTOM_DIRS', 'CREATE_CUSTOM_DIRS', 'DELETE_FILE_ON_TRASHCAN', 'DEFAULT_OPTION_PLAYLIST_STRICT_MODE', 'HTTPS', 'ENABLE_ACCESSLOG','YTDL_OPTIONS_FILE_RELOAD')
def __init__(self):
for k, v in self._DEFAULTS.items():
@ -92,6 +96,33 @@ class Config:
sys.exit(1)
self.YTDL_OPTIONS.update(opts)
if self.YTDL_OPTIONS_FILE_RELOAD and not self.YTDL_OPTIONS_FILE:
log.error('YTDL_OPTIONS_FILE_RELOAD is enabled but YTDL_OPTIONS_FILE is not set. ')
sys.exit(1)
def load_ytdl_options_file(self) -> tuple[bool, str]:
msg = ''
if not self.YTDL_OPTIONS_FILE:
msg='YTDL_OPTIONS_FILE is not set'
log.error(msg)
return (False, msg)
log.info(f'Loading yt-dlp custom options from "{self.YTDL_OPTIONS_FILE}"')
if not os.path.exists(self.YTDL_OPTIONS_FILE):
msg = f'File "{self.YTDL_OPTIONS_FILE}" not found'
log.error(msg)
return (False, msg)
try:
with open(self.YTDL_OPTIONS_FILE) as json_data:
opts = json.load(json_data)
assert isinstance(opts, dict)
except (json.decoder.JSONDecodeError, AssertionError):
msg = 'YTDL_OPTIONS_FILE contents is invalid'
log.error(msg)
return (False, msg)
self.YTDL_OPTIONS.update(opts)
return (True, msg)
config = Config()
class ObjectSerializer(json.JSONEncoder):
@ -131,6 +162,31 @@ class Notifier(DownloadQueueNotifier):
dqueue = DownloadQueue(config, Notifier())
app.on_startup.append(lambda app: dqueue.initialize())
class FileOpsFilter(DefaultFilter):
def __call__(self, change_type: int, path: str) -> bool:
return (os.path.samefile(path, config.YTDL_OPTIONS_FILE) and
change_type in (Change.modified,Change.added))
def get_options_update_time(success=True, msg=''):
result = {
'success': success,
'msg': msg,
'update_time': os.path.getmtime(config.YTDL_OPTIONS_FILE)
}
return result
async def watch_files():
path_to_watch = Path(config.YTDL_OPTIONS_FILE).resolve()
async def _watch_files():
async for changes in awatch(path_to_watch, watch_filter=FileOpsFilter()):
success, msg = config.load_ytdl_options_file()
result = get_options_update_time(success, msg)
await sio.emit('ytdl_options_changed', serializer.encode(result))
log.info(f'Starting Watch File: {path_to_watch}')
asyncio.create_task(_watch_files())
if config.YTDL_OPTIONS_FILE_RELOAD:
app.on_startup.append(lambda app: watch_files())
@routes.post(config.URL_PREFIX + 'add')
async def add(request):
log.info("Received request to add download")
@ -203,6 +259,8 @@ async def connect(sid, environ):
await sio.emit('configuration', serializer.encode(config), to=sid)
if config.CUSTOM_DIRS:
await sio.emit('custom_dirs', serializer.encode(get_custom_dirs()), to=sid)
if config.YTDL_OPTIONS_FILE_RELOAD:
await sio.emit('ytdl_options_changed', serializer.encode(get_options_update_time()), to=sid)
def get_custom_dirs():
def recursive_dirs(base):

View File

@ -388,6 +388,11 @@
<span class="version-value">{{metubeVersion}}</span>
</div>
<div class="version-separator"></div>
<div class="version-item" *ngIf="ytDlpOptionsUpdateTime">
<span class="version-label">yt-dlp-options</span>
<span class="version-value">{{ytDlpOptionsUpdateTime}}</span>
</div>
<div class="version-separator" *ngIf="ytDlpOptionsUpdateTime"></div>
<a href="https://github.com/alexta69/metube" target="_blank" class="github-link">
<fa-icon [icon]="faGithub"></fa-icon>
<span>GitHub</span>

View File

@ -39,6 +39,7 @@ export class AppComponent implements AfterViewInit {
batchImportStatus = '';
importInProgress = false;
cancelImportFlag = false;
ytDlpOptionsUpdateTime: string | null = null;
ytDlpVersion: string | null = null;
metubeVersion: string | null = null;
isAdvancedOpen = false;
@ -101,6 +102,7 @@ export class AppComponent implements AfterViewInit {
ngOnInit() {
this.getConfiguration();
this.getYtdlOptionsUpdateTime();
this.customDirs$ = this.getMatchingCustomDir();
this.setTheme(this.activeTheme);
@ -174,6 +176,18 @@ export class AppComponent implements AfterViewInit {
);
}
getYtdlOptionsUpdateTime() {
this.downloads.ytdlOptionsChanged.subscribe({
next: (data) => {
if (data['success']){
const date = new Date(data['update_time'] * 1000);
this.ytDlpOptionsUpdateTime=date.toLocaleString();
}else{
alert("Error reload yt-dlp options: "+data['msg']);
}
}
});
}
getConfiguration() {
this.downloads.configurationChanged.subscribe({
next: (config) => {

View File

@ -39,6 +39,7 @@ export class DownloadsService {
queueChanged = new Subject();
doneChanged = new Subject();
customDirsChanged = new Subject();
ytdlOptionsChanged = new Subject();
configurationChanged = new Subject();
updated = new Subject();
@ -98,6 +99,10 @@ export class DownloadsService {
this.customDirs = data;
this.customDirsChanged.next(data);
});
socket.fromEvent('ytdl_options_changed').subscribe((strdata: string) => {
let data = JSON.parse(strdata);
this.ytdlOptionsChanged.next(data);
});
}
handleHTTPError(error: HttpErrorResponse) {