diff --git a/README.md b/README.md index 53291af..b3a25f6 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,12 @@ Certain values can be set via environment variables, using the `-e` parameter on * __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. * __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. +* __DOWNLOAD_MODE__ :This flag controls how downloads are scheduled and executed. Options are `sequential`, `concurrent`, and `limited`. Defaults to `limited`. Can also be configured in the UI: + * `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. * `concurrent`: Downloads are started immediately as they are added, with no built-in limit on how many run simultaneously. This mode may overwhelm your system if too many downloads start at once. * `limited`: Downloads are started concurrently but are capped by a concurrency limit. In this mode, a semaphore is used so that at most a fixed number of downloads run at any given time. * **MAX\_CONCURRENT\_DOWNLOADS** This flag is used only when **DOWNLOAD\_MODE** is set to **limited**. - It specifies the maximum number of simultaneous downloads allowed. For example, if set to `5`, then at most five downloads will run concurrently, and any additional downloads will wait until one of the active downloads completes. Defaults to `3`. + It specifies the maximum number of simultaneous downloads allowed. For example, if set to `5`, then at most five downloads will run concurrently, and any additional downloads will wait until one of the active downloads completes. Defaults to `3`. Can also be configured in the UI. * __LOGLEVEL__: Log level, can be set to `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` or `NONE`. Defaults to `INFO`. * __ENABLE_ACCESSLOG__: whether to enable access log. Defaults to `false`. diff --git a/app/main.py b/app/main.py index 3ba5ae4..f132fb9 100644 --- a/app/main.py +++ b/app/main.py @@ -261,11 +261,63 @@ async def history(request): log.info("Sending download history") return web.Response(text=serializer.encode(history)) +@routes.post(config.URL_PREFIX + 'update_download_config') +async def update_download_config(request): + post = await request.json() + download_mode = post.get('download_mode') + max_concurrent = post.get('max_concurrent_downloads') + + if download_mode not in ['sequential', 'concurrent', 'limited']: + raise web.HTTPBadRequest(text='Invalid download_mode') + + if max_concurrent is not None: + try: + max_concurrent = int(max_concurrent) + if max_concurrent < 1: + raise ValueError() + except ValueError: + raise web.HTTPBadRequest(text='max_concurrent_downloads must be a positive integer') + + # Update config + config.DOWNLOAD_MODE = download_mode + if max_concurrent is not None: + config.MAX_CONCURRENT_DOWNLOADS = max_concurrent + + # Update download queue configuration + if download_mode == 'sequential': + if not hasattr(dqueue, 'seq_lock'): + dqueue.seq_lock = asyncio.Lock() + dqueue.semaphore = None + elif download_mode == 'limited': + dqueue.semaphore = asyncio.Semaphore(int(config.MAX_CONCURRENT_DOWNLOADS)) + if hasattr(dqueue, 'seq_lock'): + delattr(dqueue, 'seq_lock') + else: # concurrent + dqueue.semaphore = None + if hasattr(dqueue, 'seq_lock'): + delattr(dqueue, 'seq_lock') + + # Notify all clients of the configuration change + await sio.emit('configuration', serializer.encode(config)) + + return web.Response(text=serializer.encode({'status': 'ok'})) + @sio.event async def connect(sid, environ): log.info(f"Client connected: {sid}") await sio.emit('all', serializer.encode(dqueue.get()), to=sid) - await sio.emit('configuration', serializer.encode(config), to=sid) + # Include download settings in configuration + config_dict = { + 'CUSTOM_DIRS': config.CUSTOM_DIRS, + 'CREATE_CUSTOM_DIRS': config.CREATE_CUSTOM_DIRS, + 'DEFAULT_OPTION_PLAYLIST_STRICT_MODE': config.DEFAULT_OPTION_PLAYLIST_STRICT_MODE, + 'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT': config.DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT, + 'PUBLIC_HOST_URL': config.PUBLIC_HOST_URL, + 'PUBLIC_HOST_AUDIO_URL': config.PUBLIC_HOST_AUDIO_URL, + 'DOWNLOAD_MODE': config.DOWNLOAD_MODE, + 'MAX_CONCURRENT_DOWNLOADS': config.MAX_CONCURRENT_DOWNLOADS + } + await sio.emit('configuration', serializer.encode(config_dict), to=sid) if config.CUSTOM_DIRS: await sio.emit('custom_dirs', serializer.encode(get_custom_dirs()), to=sid) if config.YTDL_OPTIONS_FILE: diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index a536154..cda355a 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -207,6 +207,34 @@ + +