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:
commit
f83dd6a951
3 changed files with 69 additions and 23 deletions
70
app/ytdl.py
70
app/ytdl.py
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue