This commit is contained in:
Elad Levi 2025-09-24 16:40:11 +03:00 committed by GitHub
commit 3ead3bca4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 129 additions and 5 deletions

View file

@ -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 wont 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`.

View file

@ -264,11 +264,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:

View 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">

View file

@ -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();
}
} }

View file

@ -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)
);
}
} }