Merge pull request #750 from xerdream/feat/multiple-video-formats

Add "Format" column to frontend interface to display downloaded audio/video formats
This commit is contained in:
Alex 2025-08-22 23:34:13 +03:00 committed by GitHub
commit f83dd6a951
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 69 additions and 23 deletions

View file

@ -33,6 +33,7 @@ class DownloadQueueNotifier:
class DownloadInfo: class DownloadInfo:
def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, error): def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, error):
self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}' self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}'
self.id = f'{self.id}.{format}'
self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}' self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}'
self.url = url self.url = url
self.quality = quality self.quality = quality
@ -51,8 +52,8 @@ class Download:
def __init__(self, download_dir, temp_dir, output_template, output_template_chapter, quality, format, ytdl_opts, info): def __init__(self, download_dir, temp_dir, output_template, output_template_chapter, quality, format, ytdl_opts, info):
self.download_dir = download_dir self.download_dir = download_dir
self.temp_dir = temp_dir self.temp_dir = temp_dir
self.output_template = output_template self.output_template = self._add_format_identifier(format, output_template)
self.output_template_chapter = output_template_chapter self.output_template_chapter = self._add_format_identifier(format, output_template_chapter)
self.format = get_format(format, quality) self.format = get_format(format, quality)
self.ytdl_opts = get_opts(format, quality, ytdl_opts) self.ytdl_opts = get_opts(format, quality, ytdl_opts)
if "impersonate" in self.ytdl_opts: if "impersonate" in self.ytdl_opts:
@ -139,6 +140,8 @@ class Download:
if self.status_queue is not None: if self.status_queue is not None:
self.status_queue.put(None) self.status_queue.put(None)
self._delete_format_identifier()
def running(self): def running(self):
try: try:
return self.proc is not None and self.proc.is_alive() return self.proc is not None and self.proc.is_alive()
@ -174,6 +177,49 @@ class Download:
self.info.eta = status.get('eta') self.info.eta = status.get('eta')
log.info(f"Updating status for {self.info.title}: {status}") log.info(f"Updating status for {self.info.title}: {status}")
await self.notifier.updated(self.info) await self.notifier.updated(self.info)
def _add_format_identifier(self, identifier, template):
# Preventing the post-processing of YT-DLP from deleting the intermediate file which was download before.
return f'{identifier}_{template}'
def _delete_format_identifier(self):
# Delete the identifier in the file name after the post-processing is complete.
if self.canceled or self.info.status != 'finished' or not hasattr(self.info,'filename'):
return
try:
filename = re.sub(r'^\w+_', '', self.info.filename)
filepath_idt = os.path.join(self.download_dir, self.info.filename)
filepath = os.path.join(self.download_dir, filename)
if os.path.exists(filepath):
os.remove(filepath)
os.rename(filepath_idt, filepath)
log.info(f"Renamed file '{filepath_idt}' to '{filepath}'")
except PermissionError as e:
log.warning(f"Error deleting old file '{filepath}': {e} ")
return
except Exception as e:
log.warning(f"Error renaming file '{filepath_idt}': {e} ")
return
self.info.filename = filename
def delete_tmpfile(self):
if not self.tmpfilename or not self.download_dir:
return
if not os.path.isdir(self.download_dir):
return
tmpfilename = os.path.basename(self.tmpfilename)
def is_tmpfile(filename):
return filename.startswith(tmpfilename)
try:
tmpfiles = filter(is_tmpfile, os.listdir(self.download_dir))
for tmpfile in tmpfiles:
os.remove(os.path.join(self.download_dir, tmpfile))
except Exception as e:
log.warning(f"Error deleting temporary files: {e}")
class PersistentQueue: class PersistentQueue:
def __init__(self, path): def __init__(self, path):
@ -203,7 +249,7 @@ class PersistentQueue:
return sorted(shelf.items(), key=lambda item: item[1].timestamp) return sorted(shelf.items(), key=lambda item: item[1].timestamp)
def put(self, value): def put(self, value):
key = value.info.url key = value.info.id
self.dict[key] = value self.dict[key] = value
with shelve.open(self.path, 'w') as shelf: with shelve.open(self.path, 'w') as shelf:
shelf[key] = value.info shelf[key] = value.info
@ -278,17 +324,13 @@ class DownloadQueue:
def _post_download_cleanup(self, download): def _post_download_cleanup(self, download):
if download.info.status != 'finished': if download.info.status != 'finished':
if download.tmpfilename and os.path.isfile(download.tmpfilename): download.delete_tmpfile()
try:
os.remove(download.tmpfilename)
except:
pass
download.info.status = 'error' download.info.status = 'error'
download.close() download.close()
if self.queue.exists(download.info.url): if self.queue.exists(download.info.id):
self.queue.delete(download.info.url) self.queue.delete(download.info.id)
if download.canceled: if download.canceled:
asyncio.create_task(self.notifier.canceled(download.info.url)) asyncio.create_task(self.notifier.canceled(download.info.id))
else: else:
self.done.put(download) self.done.put(download)
asyncio.create_task(self.notifier.completed(download.info)) asyncio.create_task(self.notifier.completed(download.info))
@ -361,9 +403,9 @@ class DownloadQueue:
return {'status': 'ok'} return {'status': 'ok'}
elif etype == 'video' or (etype.startswith('url') and 'id' in entry and 'title' in entry): elif etype == 'video' or (etype.startswith('url') and 'id' in entry and 'title' in entry):
log.debug('Processing as a video') log.debug('Processing as a video')
key = entry.get('webpage_url') or entry['url'] url = entry.get('webpage_url') or entry['url']
if not self.queue.exists(key): dl = DownloadInfo(entry['id'], entry.get('title') or entry['id'], url, quality, format, folder, custom_name_prefix, error)
dl = DownloadInfo(entry['id'], entry.get('title') or entry['id'], key, quality, format, folder, custom_name_prefix, error) if not self.queue.exists(dl.id):
dldirectory, error_message = self.__calc_download_path(quality, format, folder) dldirectory, error_message = self.__calc_download_path(quality, format, folder)
if error_message is not None: if error_message is not None:
return error_message return error_message

View file

@ -290,7 +290,7 @@
<th scope="col" style="width: 1rem;"> <th scope="col" style="width: 1rem;">
<app-master-checkbox #queueMasterCheckbox [id]="'queue'" [list]="downloads.queue" (changed)="queueSelectionChanged($event)"></app-master-checkbox> <app-master-checkbox #queueMasterCheckbox [id]="'queue'" [list]="downloads.queue" (changed)="queueSelectionChanged($event)"></app-master-checkbox>
</th> </th>
<th scope="col">Video</th> <th scope="col">Media</th>
<th scope="col" style="width: 8rem;">Speed</th> <th scope="col" style="width: 8rem;">Speed</th>
<th scope="col" style="width: 7rem;">ETA</th> <th scope="col" style="width: 7rem;">ETA</th>
<th scope="col" style="width: 6rem;"></th> <th scope="col" style="width: 6rem;"></th>
@ -336,7 +336,8 @@
<th scope="col" style="width: 1rem;"> <th scope="col" style="width: 1rem;">
<app-master-checkbox #doneMasterCheckbox [id]="'done'" [list]="downloads.done" (changed)="doneSelectionChanged($event)"></app-master-checkbox> <app-master-checkbox #doneMasterCheckbox [id]="'done'" [list]="downloads.done" (changed)="doneSelectionChanged($event)"></app-master-checkbox>
</th> </th>
<th scope="col">Video</th> <th scope="col">Media</th>
<th scope="col">Format</th>
<th scope="col">File Size</th> <th scope="col">File Size</th>
<th scope="col" style="width: 8rem;"></th> <th scope="col" style="width: 8rem;"></th>
</tr> </tr>
@ -358,6 +359,9 @@
<span *ngIf="download.value.error"><br>Error: {{download.value.error}}</span> <span *ngIf="download.value.error"><br>Error: {{download.value.error}}</span>
</ng-template> </ng-template>
</td> </td>
<td>
<span *ngIf="download.value.format">{{ download.value.format }}</span>
</td>
<td> <td>
<span *ngIf="download.value.size">{{ download.value.size | fileSize }}</span> <span *ngIf="download.value.size">{{ download.value.size | fileSize }}</span>
</td> </td>

View file

@ -59,21 +59,21 @@ export class DownloadsService {
}); });
socket.fromEvent('added').subscribe((strdata: string) => { socket.fromEvent('added').subscribe((strdata: string) => {
let data: Download = JSON.parse(strdata); let data: Download = JSON.parse(strdata);
this.queue.set(data.url, data); this.queue.set(data.id, data);
this.queueChanged.next(null); this.queueChanged.next(null);
}); });
socket.fromEvent('updated').subscribe((strdata: string) => { socket.fromEvent('updated').subscribe((strdata: string) => {
let data: Download = JSON.parse(strdata); let data: Download = JSON.parse(strdata);
let dl: Download = this.queue.get(data.url); let dl: Download = this.queue.get(data.id);
data.checked = dl.checked; data.checked = dl.checked;
data.deleting = dl.deleting; data.deleting = dl.deleting;
this.queue.set(data.url, data); this.queue.set(data.id, data);
this.updated.next(null); this.updated.next(null);
}); });
socket.fromEvent('completed').subscribe((strdata: string) => { socket.fromEvent('completed').subscribe((strdata: string) => {
let data: Download = JSON.parse(strdata); let data: Download = JSON.parse(strdata);
this.queue.delete(data.url); this.queue.delete(data.id);
this.done.set(data.url, data); this.done.set(data.id, data);
this.queueChanged.next(null); this.queueChanged.next(null);
this.doneChanged.next(null); this.doneChanged.next(null);
}); });
@ -127,13 +127,13 @@ export class DownloadsService {
public startByFilter(where: string, filter: (dl: Download) => boolean) { public startByFilter(where: string, filter: (dl: Download) => boolean) {
let ids: string[] = []; let ids: string[] = [];
this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.url) }); this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.id) });
return this.startById(ids); return this.startById(ids);
} }
public delByFilter(where: string, filter: (dl: Download) => boolean) { public delByFilter(where: string, filter: (dl: Download) => boolean) {
let ids: string[] = []; let ids: string[] = [];
this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.url) }); this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.id) });
return this.delById(where, ids); return this.delById(where, ids);
} }
public addDownloadByUrl(url: string): Promise<any> { public addDownloadByUrl(url: string): Promise<any> {