Download Refactor (#483)

# Added
- New: Cards when processing a download shows a spinner for the progress of the download

# Changed
- Changed: Downloads now always take the backend filename and are streamed in a more optimal manner, reducing the javascript processing that was needed previously.
==================================

* Started refactor of downloader to be more UX friendly and much faster.

* Completed refactor of Volume download to use a new mechanism. Downloads are streamed over and filename used exclusively from header. Backend has additional DB calls to get the Series Name information to make filenames nice.

* download service has been updated so all download functions use new event based observable. Duplicates code for downloading, but much cleaner and faster.

* Small code cleanup
This commit is contained in:
Joseph Milazzo 2021-08-11 16:01:44 -05:00 committed by GitHub
parent 855f452d14
commit 89b68bc301
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 439 additions and 92 deletions

View file

@ -0,0 +1,75 @@
import {
HttpEvent,
HttpEventType,
HttpHeaders,
HttpProgressEvent,
HttpResponse
} from "@angular/common/http";
import { Observable } from "rxjs";
import { distinctUntilChanged, scan, map, tap } from "rxjs/operators";
function isHttpResponse<T>(event: HttpEvent<T>): event is HttpResponse<T> {
return event.type === HttpEventType.Response;
}
function isHttpProgressEvent(
event: HttpEvent<unknown>
): event is HttpProgressEvent {
return (
event.type === HttpEventType.DownloadProgress ||
event.type === HttpEventType.UploadProgress
);
}
export interface Download {
content: Blob | null;
progress: number;
state: "PENDING" | "IN_PROGRESS" | "DONE";
filename?: string;
}
export function download(saver?: (b: Blob, filename: string) => void): (source: Observable<HttpEvent<Blob>>) => Observable<Download> {
return (source: Observable<HttpEvent<Blob>>) =>
source.pipe(
scan((previous: Download, event: HttpEvent<Blob>): Download => {
if (isHttpProgressEvent(event)) {
return {
progress: event.total
? Math.round((100 * event.loaded) / event.total)
: previous.progress,
state: 'IN_PROGRESS',
content: null
}
}
if (isHttpResponse(event)) {
if (saver && event.body) {
saver(event.body, getFilename(event.headers, ''))
}
return {
progress: 100,
state: 'DONE',
content: event.body,
filename: getFilename(event.headers, '')
}
}
return previous;
},
{state: 'PENDING', progress: 0, content: null}
)
)
}
function getFilename(headers: HttpHeaders, defaultName: string) {
const tokens = (headers.get('content-disposition') || '').split(';');
let filename = tokens[1].replace('filename=', '').replace(/"/ig, '').trim();
if (filename.startsWith('download_') || filename.startsWith('kavita_download_')) {
const ext = filename.substring(filename.lastIndexOf('.'), filename.length);
if (defaultName !== '') {
return defaultName + ext;
}
return filename.replace('kavita_', '').replace('download_', '');
//return defaultName + ext;
}
return filename;
}