Add UI and backend support for download configuration
Introduces UI controls to set download mode and max concurrent downloads, with backend API to update these settings dynamically. The configuration is now synchronized between frontend and backend, allowing users to adjust download scheduling directly from the UI.
This commit is contained in:
parent
ee83bb8165
commit
2141db22a8
5 changed files with 129 additions and 5 deletions
|
|
@ -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__: 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__: 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
|
* __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`:
|
* __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.
|
* `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.
|
* `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.
|
* `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**.
|
* **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`.
|
* __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`.
|
* __ENABLE_ACCESSLOG__: whether to enable access log. Defaults to `false`.
|
||||||
|
|
||||||
|
|
|
||||||
54
app/main.py
54
app/main.py
|
|
@ -261,11 +261,63 @@ async def history(request):
|
||||||
log.info("Sending download history")
|
log.info("Sending download history")
|
||||||
return web.Response(text=serializer.encode(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
|
@sio.event
|
||||||
async def connect(sid, environ):
|
async def connect(sid, environ):
|
||||||
log.info(f"Client connected: {sid}")
|
log.info(f"Client connected: {sid}")
|
||||||
await sio.emit('all', serializer.encode(dqueue.get()), to=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:
|
if config.CUSTOM_DIRS:
|
||||||
await sio.emit('custom_dirs', serializer.encode(get_custom_dirs()), to=sid)
|
await sio.emit('custom_dirs', serializer.encode(get_custom_dirs()), to=sid)
|
||||||
if config.YTDL_OPTIONS_FILE:
|
if config.YTDL_OPTIONS_FILE:
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,34 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Download Configuration -->
|
||||||
|
<div class="row mt-3" *ngIf="isAdvancedOpen">
|
||||||
|
<div class="col">
|
||||||
|
<h6>Download Configuration</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="downloadMode" class="form-label">Download Mode</label>
|
||||||
|
<select id="downloadMode" class="form-select" [(ngModel)]="downloadMode" (change)="onDownloadModeChange()">
|
||||||
|
<option *ngFor="let mode of availableDownloadModes" [value]="mode.id">{{mode.name}}</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
<small>Sequential: Downloads one at a time<br>
|
||||||
|
Limited: Downloads up to the specified number concurrently<br>
|
||||||
|
Unlimited: No limit on concurrent downloads</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6" *ngIf="downloadMode === 'limited'">
|
||||||
|
<label for="maxConcurrent" class="form-label">Max Concurrent Downloads</label>
|
||||||
|
<input type="number" id="maxConcurrent" class="form-control" min="1" max="20"
|
||||||
|
[(ngModel)]="maxConcurrentDownloads" (change)="onMaxConcurrentChange()">
|
||||||
|
<div class="form-text">
|
||||||
|
<small>Maximum number of simultaneous downloads (1-20)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Advanced Actions -->
|
<!-- Advanced Actions -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,15 @@ export class AppComponent implements AfterViewInit {
|
||||||
metubeVersion: string | null = null;
|
metubeVersion: string | null = null;
|
||||||
isAdvancedOpen = false;
|
isAdvancedOpen = false;
|
||||||
|
|
||||||
|
// Download configuration
|
||||||
|
downloadMode: string = 'limited';
|
||||||
|
maxConcurrentDownloads: number = 3;
|
||||||
|
availableDownloadModes = [
|
||||||
|
{ id: 'sequential', name: 'Sequential (one at a time)' },
|
||||||
|
{ id: 'limited', name: 'Limited concurrent' },
|
||||||
|
{ id: 'concurrent', name: 'Unlimited concurrent' }
|
||||||
|
];
|
||||||
|
|
||||||
// Download metrics
|
// Download metrics
|
||||||
activeDownloads = 0;
|
activeDownloads = 0;
|
||||||
queuedDownloads = 0;
|
queuedDownloads = 0;
|
||||||
|
|
@ -196,6 +205,14 @@ export class AppComponent implements AfterViewInit {
|
||||||
if (playlistItemLimit !== '0') {
|
if (playlistItemLimit !== '0') {
|
||||||
this.playlistItemLimit = playlistItemLimit;
|
this.playlistItemLimit = playlistItemLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update download configuration
|
||||||
|
if (config['DOWNLOAD_MODE']) {
|
||||||
|
this.downloadMode = config['DOWNLOAD_MODE'];
|
||||||
|
}
|
||||||
|
if (config['MAX_CONCURRENT_DOWNLOADS']) {
|
||||||
|
this.maxConcurrentDownloads = config['MAX_CONCURRENT_DOWNLOADS'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -516,4 +533,24 @@ export class AppComponent implements AfterViewInit {
|
||||||
|
|
||||||
this.totalSpeed = downloadingItems.reduce((total, item) => total + (item.speed || 0), 0);
|
this.totalSpeed = downloadingItems.reduce((total, item) => total + (item.speed || 0), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateDownloadConfig() {
|
||||||
|
this.downloads.updateDownloadConfig(this.downloadMode, this.maxConcurrentDownloads)
|
||||||
|
.subscribe((status: Status) => {
|
||||||
|
if (status.status === 'error') {
|
||||||
|
alert(`Error updating download configuration: ${status.msg}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadModeChange() {
|
||||||
|
this.updateDownloadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMaxConcurrentChange() {
|
||||||
|
if (this.maxConcurrentDownloads < 1) {
|
||||||
|
this.maxConcurrentDownloads = 1;
|
||||||
|
}
|
||||||
|
this.updateDownloadConfig();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -157,5 +157,12 @@ export class DownloadsService {
|
||||||
return Array.from(this.queue.values()).map(download => download.url);
|
return Array.from(this.queue.values()).map(download => download.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateDownloadConfig(downloadMode: string, maxConcurrentDownloads: number): Observable<Status> {
|
||||||
|
return this.http.post<Status>('update_download_config', {
|
||||||
|
download_mode: downloadMode,
|
||||||
|
max_concurrent_downloads: maxConcurrentDownloads
|
||||||
|
}).pipe(
|
||||||
|
catchError(this.handleHTTPError)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue