Merge pull request #38 from Rpsl/retry_button

Added retry button for failed download
This commit is contained in:
Alex 2021-07-29 21:17:07 +03:00 committed by GitHub
commit 09da20881c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 56 additions and 14 deletions

View File

@ -25,8 +25,9 @@ class DownloadQueueNotifier:
raise NotImplementedError raise NotImplementedError
class DownloadInfo: class DownloadInfo:
def __init__(self, id, title, url): def __init__(self, id, title, url, quality):
self.id, self.title, self.url = id, title, url self.id, self.title, self.url = id, title, url
self.quality = quality
self.status = self.msg = self.percent = self.speed = self.eta = None self.status = self.msg = self.percent = self.speed = self.eta = None
class Download: class Download:
@ -53,7 +54,7 @@ class Download:
self.proc = None self.proc = None
self.loop = None self.loop = None
self.notifier = None self.notifier = None
def _download(self): def _download(self):
try: try:
ret = youtube_dl.YoutubeDL(params={ ret = youtube_dl.YoutubeDL(params={
@ -69,7 +70,7 @@ class Download:
self.status_queue.put({'status': 'finished' if ret == 0 else 'error'}) self.status_queue.put({'status': 'finished' if ret == 0 else 'error'})
except youtube_dl.utils.YoutubeDLError as exc: except youtube_dl.utils.YoutubeDLError as exc:
self.status_queue.put({'status': 'error', 'msg': str(exc)}) self.status_queue.put({'status': 'error', 'msg': str(exc)})
async def start(self, notifier): async def start(self, notifier):
if Download.manager is None: if Download.manager is None:
Download.manager = multiprocessing.Manager() Download.manager = multiprocessing.Manager()
@ -81,7 +82,7 @@ class Download:
self.info.status = 'preparing' self.info.status = 'preparing'
await self.notifier.updated(self.info) await self.notifier.updated(self.info)
asyncio.ensure_future(self.update_status()) asyncio.ensure_future(self.update_status())
return await self.loop.run_in_executor(None, self.proc.join) return await self.loop.run_in_executor(None, self.proc.join)
def cancel(self): def cancel(self):
if self.running(): if self.running():
@ -147,7 +148,7 @@ class DownloadQueue:
return {'status': 'ok'} return {'status': 'ok'}
elif etype == 'video' or etype.startswith('url') and 'id' in entry: elif etype == 'video' or etype.startswith('url') and 'id' in entry:
if entry['id'] not in self.queue: if entry['id'] not in self.queue:
dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url']) dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url'], quality)
dldirectory = self.config.DOWNLOAD_DIR if quality != 'audio' else self.config.AUDIO_DOWNLOAD_DIR dldirectory = self.config.DOWNLOAD_DIR if quality != 'audio' else self.config.AUDIO_DOWNLOAD_DIR
self.queue[entry['id']] = Download(dldirectory, self.config.OUTPUT_TEMPLATE, quality, dl) self.queue[entry['id']] = Download(dldirectory, self.config.OUTPUT_TEMPLATE, quality, dl)
self.event.set() self.event.set()
@ -170,7 +171,7 @@ class DownloadQueue:
except youtube_dl.utils.YoutubeDLError as exc: except youtube_dl.utils.YoutubeDLError as exc:
return {'status': 'error', 'msg': str(exc)} return {'status': 'error', 'msg': str(exc)}
return await self.__add_entry(entry, quality, already) return await self.__add_entry(entry, quality, already)
async def cancel(self, ids): async def cancel(self, ids):
for id in ids: for id in ids:
if id not in self.queue: if id not in self.queue:
@ -195,7 +196,7 @@ class DownloadQueue:
def get(self): def get(self):
return(list((k, v.info) for k, v in self.queue.items()), return(list((k, v.info) for k, v in self.queue.items()),
list((k, v.info) for k, v in self.done.items())) list((k, v.info) for k, v in self.done.items()))
async def __download(self): async def __download(self):
while True: while True:
while not self.queue: while not self.queue:

23
ui/package-lock.json generated
View File

@ -20,6 +20,7 @@
"@fortawesome/angular-fontawesome": "^0.7.0", "@fortawesome/angular-fontawesome": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^1.2.28", "@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-regular-svg-icons": "^5.15.2", "@fortawesome/free-regular-svg-icons": "^5.15.2",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@ng-bootstrap/ng-bootstrap": "^8.0.4", "@ng-bootstrap/ng-bootstrap": "^8.0.4",
"bootstrap": "^4.5.0", "bootstrap": "^4.5.0",
"ngx-socket-io": "~3.2.0", "ngx-socket-io": "~3.2.0",
@ -1958,6 +1959,18 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "5.15.3",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz",
"integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "^0.2.35"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@istanbuljs/schema": { "node_modules/@istanbuljs/schema": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@ -18662,6 +18675,14 @@
"@fortawesome/fontawesome-common-types": "^0.2.35" "@fortawesome/fontawesome-common-types": "^0.2.35"
} }
}, },
"@fortawesome/free-solid-svg-icons": {
"version": "5.15.3",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz",
"integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.35"
}
},
"@istanbuljs/schema": { "@istanbuljs/schema": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@ -20257,6 +20278,8 @@
"integrity": "sha512-v3+E0Ucu2xWJMOJ2fA/q9pDT/hlxHftHGPUay1/1cTgyPV5JTHFdO9hqo837Sx2s9vKBMTt5gO+lhF95PO6J+g==", "integrity": "sha512-v3+E0Ucu2xWJMOJ2fA/q9pDT/hlxHftHGPUay1/1cTgyPV5JTHFdO9hqo837Sx2s9vKBMTt5gO+lhF95PO6J+g==",
"dev": true, "dev": true,
"requires": { "requires": {
"@angular/compiler": "9.0.0",
"@angular/core": "9.0.0",
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"aria-query": "^3.0.0", "aria-query": "^3.0.0",
"axobject-query": "2.0.2", "axobject-query": "2.0.2",

View File

@ -23,6 +23,7 @@
"@fortawesome/angular-fontawesome": "^0.7.0", "@fortawesome/angular-fontawesome": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^1.2.28", "@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-regular-svg-icons": "^5.15.2", "@fortawesome/free-regular-svg-icons": "^5.15.2",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@ng-bootstrap/ng-bootstrap": "^8.0.4", "@ng-bootstrap/ng-bootstrap": "^8.0.4",
"bootstrap": "^4.5.0", "bootstrap": "^4.5.0",
"ngx-socket-io": "~3.2.0", "ngx-socket-io": "~3.2.0",

View File

@ -39,7 +39,7 @@
<tr> <tr>
<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"> <th scope="col">
<button type="button" class="btn btn-link px-0 mr-4" disabled #queueDelSelected (click)="delSelectedDownloads('queue')"><fa-icon [icon]="faTrashAlt"></fa-icon>&nbsp; Cancel selected</button> <button type="button" class="btn btn-link px-0 mr-4" disabled #queueDelSelected (click)="delSelectedDownloads('queue')"><fa-icon [icon]="faTrashAlt"></fa-icon>&nbsp; Cancel selected</button>
</th> </th>
@ -69,13 +69,14 @@
<tr> <tr>
<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"> <th scope="col">
<button type="button" class="btn btn-link px-0 mr-4" disabled #doneDelSelected (click)="delSelectedDownloads('done')"><fa-icon [icon]="faTrashAlt"></fa-icon>&nbsp; Clear selected</button> <button type="button" class="btn btn-link px-0 mr-4" disabled #doneDelSelected (click)="delSelectedDownloads('done')"><fa-icon [icon]="faTrashAlt"></fa-icon>&nbsp; Clear selected</button>
<button type="button" class="btn btn-link px-0 mr-4" disabled #doneClearCompleted (click)="clearCompletedDownloads()"><fa-icon [icon]="faCheckCircle"></fa-icon>&nbsp; Clear completed</button> <button type="button" class="btn btn-link px-0 mr-4" disabled #doneClearCompleted (click)="clearCompletedDownloads()"><fa-icon [icon]="faCheckCircle"></fa-icon>&nbsp; Clear completed</button>
<button type="button" class="btn btn-link px-0 mr-4" disabled #doneClearFailed (click)="clearFailedDownloads()"><fa-icon [icon]="faTimesCircle"></fa-icon>&nbsp; Clear failed</button> <button type="button" class="btn btn-link px-0 mr-4" disabled #doneClearFailed (click)="clearFailedDownloads()"><fa-icon [icon]="faTimesCircle"></fa-icon>&nbsp; Clear failed</button>
</th> </th>
<th scope="col" style="width: 2rem;"></th> <th scope="col" style="width: 2rem;"></th>
<th scope="col" style="width: 2rem;"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -90,9 +91,14 @@
</div> </div>
<span ngbTooltip="{{download.value.msg}}">{{ download.value.title }}</span> <span ngbTooltip="{{download.value.msg}}">{{ download.value.title }}</span>
</td> </td>
<td><button type="button" class="btn btn-link" (click)="delDownload('done', download.key)"><fa-icon [icon]="faTrashAlt"></fa-icon></button></td> <td>
<button *ngIf="download.value.status == 'error'" type="button" class="btn btn-link" (click)="retryDownload(download.key, download.value.quality)"><fa-icon [icon]="faRedoAlt"></fa-icon></button>
</td>
<td>
<button type="button" class="btn btn-link" (click)="delDownload('done', download.key)"><fa-icon [icon]="faTrashAlt"></fa-icon></button>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</main><!-- /.container --> </main><!-- /.container -->

View File

@ -1,5 +1,6 @@
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { faTrashAlt, faCheckCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; import { faTrashAlt, faCheckCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons';
import { faRedoAlt } from '@fortawesome/free-solid-svg-icons';
import { DownloadsService, Status } from './downloads.service'; import { DownloadsService, Status } from './downloads.service';
import { MasterCheckboxComponent } from './master-checkbox.component'; import { MasterCheckboxComponent } from './master-checkbox.component';
@ -21,7 +22,7 @@ export class AppComponent implements AfterViewInit {
]; ];
quality: string = "best"; quality: string = "best";
addInProgress = false; addInProgress = false;
@ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent; @ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent;
@ViewChild('queueDelSelected') queueDelSelected: ElementRef; @ViewChild('queueDelSelected') queueDelSelected: ElementRef;
@ViewChild('doneMasterCheckbox') doneMasterCheckbox: MasterCheckboxComponent; @ViewChild('doneMasterCheckbox') doneMasterCheckbox: MasterCheckboxComponent;
@ -32,6 +33,7 @@ export class AppComponent implements AfterViewInit {
faTrashAlt = faTrashAlt; faTrashAlt = faTrashAlt;
faCheckCircle = faCheckCircle; faCheckCircle = faCheckCircle;
faTimesCircle = faTimesCircle; faTimesCircle = faTimesCircle;
faRedoAlt = faRedoAlt;
constructor(public downloads: DownloadsService) { constructor(public downloads: DownloadsService) {
} }
@ -68,9 +70,12 @@ export class AppComponent implements AfterViewInit {
this.doneDelSelected.nativeElement.disabled = checked == 0; this.doneDelSelected.nativeElement.disabled = checked == 0;
} }
addDownload() { addDownload(url?: string, quality?: string) {
url = url ?? this.addUrl
quality = quality ?? this.quality
this.addInProgress = true; this.addInProgress = true;
this.downloads.add(this.addUrl, this.quality).subscribe((status: Status) => { this.downloads.add(url, quality).subscribe((status: Status) => {
if (status.status === 'error') { if (status.status === 'error') {
alert(`Error adding URL: ${status.msg}`); alert(`Error adding URL: ${status.msg}`);
} else { } else {
@ -80,6 +85,11 @@ export class AppComponent implements AfterViewInit {
}); });
} }
retryDownload(key: string, quality:string){
this.addDownload(key, quality);
this.downloads.delById('done', [key]).subscribe();
}
delDownload(where: string, id: string) { delDownload(where: string, id: string) {
this.downloads.delById(where, [id]).subscribe(); this.downloads.delById(where, [id]).subscribe();
} }

View File

@ -15,6 +15,7 @@ interface Download {
url: string, url: string,
status: string; status: string;
msg: string; msg: string;
quality: string;
percent: number; percent: number;
speed: number; speed: number;
eta: number; eta: number;