Merge 4bbdedb741 into 420befba75
This commit is contained in:
commit
b6eb9e5668
5 changed files with 101 additions and 22 deletions
|
|
@ -220,12 +220,15 @@ async def add(request):
|
|||
format = post.get('format')
|
||||
folder = post.get('folder')
|
||||
custom_name_prefix = post.get('custom_name_prefix')
|
||||
custom_name = post.get('custom_name')
|
||||
playlist_strict_mode = post.get('playlist_strict_mode')
|
||||
playlist_item_limit = post.get('playlist_item_limit')
|
||||
auto_start = post.get('auto_start')
|
||||
|
||||
if custom_name_prefix is None:
|
||||
custom_name_prefix = ''
|
||||
if custom_name is None:
|
||||
custom_name = ''
|
||||
if auto_start is None:
|
||||
auto_start = True
|
||||
if playlist_strict_mode is None:
|
||||
|
|
@ -235,7 +238,7 @@ async def add(request):
|
|||
|
||||
playlist_item_limit = int(playlist_item_limit)
|
||||
|
||||
status = await dqueue.add(url, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start)
|
||||
status = await dqueue.add(url, quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start)
|
||||
return web.Response(text=serializer.encode(status))
|
||||
|
||||
@routes.post(config.URL_PREFIX + 'delete')
|
||||
|
|
|
|||
84
app/ytdl.py
84
app/ytdl.py
|
|
@ -14,6 +14,24 @@ from datetime import datetime
|
|||
|
||||
log = logging.getLogger('ytdl')
|
||||
|
||||
def sanitize_custom_input(input):
|
||||
# Remove or replace dangerous characters
|
||||
# Path separators and traversal patterns
|
||||
input = input.replace('/', '_')
|
||||
input = input.replace('\\', '_')
|
||||
input = input.replace('..', '_')
|
||||
|
||||
# Command injection patterns
|
||||
input = re.sub(r'[;&|`$(){}[\]<>]', '_', input)
|
||||
|
||||
# Control characters and other problematic characters
|
||||
input = re.sub(r'[\x00-\x1f\x7f-\x9f]', '_', input)
|
||||
|
||||
# Replace multiple underscores with single underscore
|
||||
input = re.sub(r'_+', '_', input)
|
||||
|
||||
return input
|
||||
|
||||
class DownloadQueueNotifier:
|
||||
async def added(self, dl):
|
||||
raise NotImplementedError
|
||||
|
|
@ -31,14 +49,30 @@ class DownloadQueueNotifier:
|
|||
raise NotImplementedError
|
||||
|
||||
class DownloadInfo:
|
||||
def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, error, entry, playlist_item_limit):
|
||||
self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}'
|
||||
self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}'
|
||||
def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, custom_name, error, entry, playlist_item_limit):
|
||||
# Sanitize custom inputs to prevent path traversal and command injection attacks
|
||||
if custom_name:
|
||||
custom_name = sanitize_custom_input(custom_name)
|
||||
if custom_name_prefix:
|
||||
custom_name_prefix = sanitize_custom_input(custom_name_prefix)
|
||||
|
||||
# Handle custom name logic - if custom_name is provided, use it; otherwise use prefix logic
|
||||
if custom_name and len(custom_name.strip()) > 0:
|
||||
self.id = id
|
||||
self.title = custom_name
|
||||
elif len(custom_name_prefix) > 0:
|
||||
self.id = f'{custom_name_prefix}.{id}'
|
||||
self.title = f'{custom_name_prefix}.{title}'
|
||||
else:
|
||||
self.id = id
|
||||
self.title = title
|
||||
|
||||
self.url = url
|
||||
self.quality = quality
|
||||
self.format = format
|
||||
self.folder = folder
|
||||
self.custom_name_prefix = custom_name_prefix
|
||||
self.custom_name = custom_name
|
||||
self.msg = self.percent = self.speed = self.eta = None
|
||||
self.status = "pending"
|
||||
self.size = None
|
||||
|
|
@ -333,12 +367,37 @@ class DownloadQueue:
|
|||
dldirectory, error_message = self.__calc_download_path(dl.quality, dl.format, dl.folder)
|
||||
if error_message is not None:
|
||||
return error_message
|
||||
output = self.config.OUTPUT_TEMPLATE if len(dl.custom_name_prefix) == 0 else f'{dl.custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}'
|
||||
# Handle custom naming in output template
|
||||
if dl.custom_name and len(dl.custom_name.strip()) > 0:
|
||||
# Create a custom output template using the custom name
|
||||
# Replace %(title)s with the custom name in the template
|
||||
output = self.config.OUTPUT_TEMPLATE.replace('%(title)s', dl.custom_name)
|
||||
elif len(dl.custom_name_prefix) > 0:
|
||||
output = f'{dl.custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}'
|
||||
else:
|
||||
output = self.config.OUTPUT_TEMPLATE
|
||||
output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER
|
||||
entry = getattr(dl, 'entry', None)
|
||||
if entry is not None and 'playlist' in entry and entry['playlist'] is not None:
|
||||
if len(self.config.OUTPUT_TEMPLATE_PLAYLIST):
|
||||
output = self.config.OUTPUT_TEMPLATE_PLAYLIST
|
||||
# Start with the playlist template
|
||||
playlist_output = self.config.OUTPUT_TEMPLATE_PLAYLIST
|
||||
|
||||
# Apply custom naming logic to playlist template
|
||||
if dl.custom_name and len(dl.custom_name.strip()) > 0:
|
||||
# If playlist has multiple entries, append entry id to custom name to avoid overwriting
|
||||
playlist_entries = entry.get('entries')
|
||||
if playlist_entries and isinstance(playlist_entries, list) and len(playlist_entries) > 1:
|
||||
entry_id = entry.get('id') or ''
|
||||
custom_name_with_id = f"{dl.custom_name}_{entry_id}" if entry_id else dl.custom_name
|
||||
output = playlist_output.replace('%(title)s', custom_name_with_id)
|
||||
else:
|
||||
output = playlist_output.replace('%(title)s', dl.custom_name)
|
||||
elif len(dl.custom_name_prefix) > 0:
|
||||
# Add prefix to the playlist template
|
||||
output = f'{dl.custom_name_prefix}.{playlist_output}'
|
||||
else:
|
||||
output = playlist_output
|
||||
for property, value in entry.items():
|
||||
if property.startswith("playlist"):
|
||||
output = output.replace(f"%({property})s", str(value))
|
||||
|
|
@ -347,6 +406,7 @@ class DownloadQueue:
|
|||
if playlist_item_limit > 0:
|
||||
log.info(f'playlist limit is set. Processing only first {playlist_item_limit} entries')
|
||||
ytdl_options['playlistend'] = playlist_item_limit
|
||||
|
||||
download = Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, dl.quality, dl.format, ytdl_options, dl)
|
||||
if auto_start is True:
|
||||
self.queue.put(download)
|
||||
|
|
@ -355,7 +415,7 @@ class DownloadQueue:
|
|||
self.pending.put(download)
|
||||
await self.notifier.added(dl)
|
||||
|
||||
async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already):
|
||||
async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start, already):
|
||||
if not entry:
|
||||
return {'status': 'error', 'msg': "Invalid/empty data was given."}
|
||||
|
||||
|
|
@ -371,7 +431,7 @@ class DownloadQueue:
|
|||
|
||||
if etype.startswith('url'):
|
||||
log.debug('Processing as an url')
|
||||
return await self.add(entry['url'], quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already)
|
||||
return await self.add(entry['url'], quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start, already)
|
||||
elif etype == 'playlist':
|
||||
log.debug('Processing as a playlist')
|
||||
entries = entry['entries']
|
||||
|
|
@ -388,7 +448,7 @@ class DownloadQueue:
|
|||
for property in ("id", "title", "uploader", "uploader_id"):
|
||||
if property in entry:
|
||||
etr[f"playlist_{property}"] = entry[property]
|
||||
results.append(await self.__add_entry(etr, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already))
|
||||
results.append(await self.__add_entry(etr, quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start, already))
|
||||
if any(res['status'] == 'error' for res in results):
|
||||
return {'status': 'error', 'msg': ', '.join(res['msg'] for res in results if res['status'] == 'error' and 'msg' in res)}
|
||||
return {'status': 'ok'}
|
||||
|
|
@ -396,13 +456,13 @@ class DownloadQueue:
|
|||
log.debug('Processing as a video')
|
||||
key = entry.get('webpage_url') or entry['url']
|
||||
if not self.queue.exists(key):
|
||||
dl = DownloadInfo(entry['id'], entry.get('title') or entry['id'], key, quality, format, folder, custom_name_prefix, error, entry, playlist_item_limit)
|
||||
dl = DownloadInfo(entry['id'], entry.get('title') or entry['id'], key, quality, format, folder, custom_name_prefix, custom_name, error, entry, playlist_item_limit)
|
||||
await self.__add_download(dl, auto_start)
|
||||
return {'status': 'ok'}
|
||||
return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'}
|
||||
|
||||
async def add(self, url, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start=True, already=None):
|
||||
log.info(f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=} {playlist_strict_mode=} {playlist_item_limit=} {auto_start=}')
|
||||
async def add(self, url, quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start=True, already=None):
|
||||
log.info(f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=} {custom_name=} {playlist_strict_mode=} {playlist_item_limit=} {auto_start=}')
|
||||
already = set() if already is None else already
|
||||
if url in already:
|
||||
log.info('recursion detected, skipping')
|
||||
|
|
@ -413,7 +473,7 @@ class DownloadQueue:
|
|||
entry = await asyncio.get_running_loop().run_in_executor(None, self.__extract_info, url, playlist_strict_mode)
|
||||
except yt_dlp.utils.YoutubeDLError as exc:
|
||||
return {'status': 'error', 'msg': str(exc)}
|
||||
return await self.__add_entry(entry, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already)
|
||||
return await self.__add_entry(entry, quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start, already)
|
||||
|
||||
async def start_pending(self, ids):
|
||||
for id in ids:
|
||||
|
|
|
|||
|
|
@ -179,6 +179,18 @@
|
|||
ngbTooltip="Add a prefix to downloaded filenames">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Custom Name</span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
placeholder="Default"
|
||||
name="customName"
|
||||
[(ngModel)]="customName"
|
||||
[disabled]="addInProgress || downloads.loading"
|
||||
ngbTooltip="Rename downloaded file (extension will be added automatically)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Items Limit</span>
|
||||
|
|
@ -193,7 +205,7 @@
|
|||
ngbTooltip="Maximum number of items to download from a playlist (0 = no limit)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="col-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export class AppComponent implements AfterViewInit {
|
|||
format: string;
|
||||
folder: string;
|
||||
customNamePrefix: string;
|
||||
customName: string;
|
||||
autoStart: boolean;
|
||||
playlistStrictMode: boolean;
|
||||
playlistItemLimit: number;
|
||||
|
|
@ -252,19 +253,20 @@ export class AppComponent implements AfterViewInit {
|
|||
this.quality = exists ? this.quality : 'best'
|
||||
}
|
||||
|
||||
addDownload(url?: string, quality?: string, format?: string, folder?: string, customNamePrefix?: string, playlistStrictMode?: boolean, playlistItemLimit?: number, autoStart?: boolean) {
|
||||
addDownload(url?: string, quality?: string, format?: string, folder?: string, customNamePrefix?: string, customName?: string, playlistStrictMode?: boolean, playlistItemLimit?: number, autoStart?: boolean) {
|
||||
url = url ?? this.addUrl
|
||||
quality = quality ?? this.quality
|
||||
format = format ?? this.format
|
||||
folder = folder ?? this.folder
|
||||
customNamePrefix = customNamePrefix ?? this.customNamePrefix
|
||||
customName = customName ?? this.customName
|
||||
playlistStrictMode = playlistStrictMode ?? this.playlistStrictMode
|
||||
playlistItemLimit = playlistItemLimit ?? this.playlistItemLimit
|
||||
autoStart = autoStart ?? this.autoStart
|
||||
|
||||
console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder+' customNamePrefix='+customNamePrefix+' playlistStrictMode='+playlistStrictMode+' playlistItemLimit='+playlistItemLimit+' autoStart='+autoStart);
|
||||
console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder+' customNamePrefix='+customNamePrefix+' customName='+customName+' playlistStrictMode='+playlistStrictMode+' playlistItemLimit='+playlistItemLimit+' autoStart='+autoStart);
|
||||
this.addInProgress = true;
|
||||
this.downloads.add(url, quality, format, folder, customNamePrefix, playlistStrictMode, playlistItemLimit, autoStart).subscribe((status: Status) => {
|
||||
this.downloads.add(url, quality, format, folder, customNamePrefix, customName, playlistStrictMode, playlistItemLimit, autoStart).subscribe((status: Status) => {
|
||||
if (status.status === 'error') {
|
||||
alert(`Error adding URL: ${status.msg}`);
|
||||
} else {
|
||||
|
|
@ -279,7 +281,7 @@ export class AppComponent implements AfterViewInit {
|
|||
}
|
||||
|
||||
retryDownload(key: string, download: Download) {
|
||||
this.addDownload(download.url, download.quality, download.format, download.folder, download.custom_name_prefix, download.playlist_strict_mode, download.playlist_item_limit, true);
|
||||
this.addDownload(download.url, download.quality, download.format, download.folder, download.custom_name_prefix, download.custom_name, download.playlist_strict_mode, download.playlist_item_limit, true);
|
||||
this.downloads.delById('done', [key]).subscribe();
|
||||
}
|
||||
|
||||
|
|
@ -397,7 +399,7 @@ export class AppComponent implements AfterViewInit {
|
|||
const url = urls[index];
|
||||
this.batchImportStatus = `Importing URL ${index + 1} of ${urls.length}: ${url}`;
|
||||
// Now pass the selected quality, format, folder, etc. to the add() method
|
||||
this.downloads.add(url, this.quality, this.format, this.folder, this.customNamePrefix,
|
||||
this.downloads.add(url, this.quality, this.format, this.folder, this.customNamePrefix, this.customName,
|
||||
this.playlistStrictMode, this.playlistItemLimit, this.autoStart)
|
||||
.subscribe({
|
||||
next: (status: Status) => {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export interface Download {
|
|||
format: string;
|
||||
folder: string;
|
||||
custom_name_prefix: string;
|
||||
custom_name: string;
|
||||
playlist_strict_mode: boolean;
|
||||
playlist_item_limit: number;
|
||||
status: string;
|
||||
|
|
@ -110,8 +111,8 @@ export class DownloadsService {
|
|||
return of({status: 'error', msg: msg})
|
||||
}
|
||||
|
||||
public add(url: string, quality: string, format: string, folder: string, customNamePrefix: string, playlistStrictMode: boolean, playlistItemLimit: number, autoStart: boolean) {
|
||||
return this.http.post<Status>('add', {url: url, quality: quality, format: format, folder: folder, custom_name_prefix: customNamePrefix, playlist_strict_mode: playlistStrictMode, playlist_item_limit: playlistItemLimit, auto_start: autoStart}).pipe(
|
||||
public add(url: string, quality: string, format: string, folder: string, customNamePrefix: string, customName: string, playlistStrictMode: boolean, playlistItemLimit: number, autoStart: boolean) {
|
||||
return this.http.post<Status>('add', {url: url, quality: quality, format: format, folder: folder, custom_name_prefix: customNamePrefix, custom_name: customName, playlist_strict_mode: playlistStrictMode, playlist_item_limit: playlistItemLimit, auto_start: autoStart}).pipe(
|
||||
catchError(this.handleHTTPError)
|
||||
);
|
||||
}
|
||||
|
|
@ -141,12 +142,13 @@ export class DownloadsService {
|
|||
const defaultFormat = 'mp4';
|
||||
const defaultFolder = '';
|
||||
const defaultCustomNamePrefix = '';
|
||||
const defaultCustomName = '';
|
||||
const defaultPlaylistStrictMode = false;
|
||||
const defaultPlaylistItemLimit = 0;
|
||||
const defaultAutoStart = true;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.add(url, defaultQuality, defaultFormat, defaultFolder, defaultCustomNamePrefix, defaultPlaylistStrictMode, defaultPlaylistItemLimit, defaultAutoStart)
|
||||
this.add(url, defaultQuality, defaultFormat, defaultFolder, defaultCustomNamePrefix, defaultCustomName, defaultPlaylistStrictMode, defaultPlaylistItemLimit, defaultAutoStart)
|
||||
.subscribe(
|
||||
response => resolve(response),
|
||||
error => reject(error)
|
||||
|
|
|
|||
Loading…
Reference in a new issue