diff --git a/Pipfile b/Pipfile index 66bb5a8..6dd7d93 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,7 @@ python-socketio = "~=5.0" yt-dlp = "*" mutagen = "*" curl-cffi = "*" +watchfiles = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index 5113749..9def9a6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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", diff --git a/README.md b/README.md index 53291af..ccf692e 100644 --- a/README.md +++ b/README.md @@ -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 won’t start until the previous one has finished. This mode is useful for conserving system resources or ensuring downloads occur in a strict order. diff --git a/app/main.py b/app/main.py index 90554ee..35a9be9 100644 --- a/app/main.py +++ b/app/main.py @@ -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): diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 87fb48a..a536154 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -388,6 +388,11 @@ {{metubeVersion}}
+
+ yt-dlp-options + {{ytDlpOptionsUpdateTime}} +
+
GitHub diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index d8362b6..1f9b144 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -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) => { diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index ebb187e..cf63a5e 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -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) {