Kavita+ Enhancements (#2616)
This commit is contained in:
parent
625c56b265
commit
dd44f55747
43 changed files with 1056 additions and 468 deletions
|
@ -12,7 +12,7 @@ import {
|
|||
tap,
|
||||
finalize,
|
||||
of,
|
||||
filter,
|
||||
filter, Subject,
|
||||
} from 'rxjs';
|
||||
import { download, Download } from '../_models/download';
|
||||
import { PageBookmark } from 'src/app/_models/readers/page-bookmark';
|
||||
|
@ -22,6 +22,10 @@ import { BytesPipe } from 'src/app/_pipes/bytes.pipe';
|
|||
import {translate} from "@ngneat/transloco";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {SAVER, Saver} from "../../_providers/saver.provider";
|
||||
import {UtilityService} from "./utility.service";
|
||||
import {CollectionTag} from "../../_models/collection-tag";
|
||||
import {RecentlyAddedItem} from "../../_models/recently-added-item";
|
||||
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
|
||||
|
||||
export const DEBOUNCE_TIME = 100;
|
||||
|
||||
|
@ -55,6 +59,7 @@ export type DownloadEntityType = 'volume' | 'chapter' | 'series' | 'bookmark' |
|
|||
*/
|
||||
export type DownloadEntity = Series | Volume | Chapter | PageBookmark[] | undefined;
|
||||
|
||||
export type QueueableDownloadType = Chapter | Volume;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -68,14 +73,33 @@ export class DownloadService {
|
|||
public SIZE_WARNING = 104_857_600;
|
||||
|
||||
private downloadsSource: BehaviorSubject<DownloadEvent[]> = new BehaviorSubject<DownloadEvent[]>([]);
|
||||
/**
|
||||
* Active Downloads
|
||||
*/
|
||||
public activeDownloads$ = this.downloadsSource.asObservable();
|
||||
|
||||
private downloadQueue: BehaviorSubject<QueueableDownloadType[]> = new BehaviorSubject<QueueableDownloadType[]>([]);
|
||||
/**
|
||||
* Queued Downloads
|
||||
*/
|
||||
public queuedDownloads$ = this.downloadQueue.asObservable();
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly httpClient = inject(HttpClient);
|
||||
private readonly utilityService = inject(UtilityService);
|
||||
|
||||
constructor(@Inject(SAVER) private save: Saver) { }
|
||||
constructor(@Inject(SAVER) private save: Saver) {
|
||||
this.downloadQueue.subscribe((queue) => {
|
||||
if (queue.length > 0) {
|
||||
const entity = queue.shift();
|
||||
console.log('Download Queue shifting entity: ', entity);
|
||||
if (entity === undefined) return;
|
||||
this.processDownload(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
@ -84,7 +108,7 @@ export class DownloadService {
|
|||
* @param downloadEntity
|
||||
* @returns
|
||||
*/
|
||||
downloadSubtitle(downloadEntityType: DownloadEntityType, downloadEntity: DownloadEntity | undefined) {
|
||||
downloadSubtitle(downloadEntityType: DownloadEntityType | undefined, downloadEntity: DownloadEntity | undefined) {
|
||||
switch (downloadEntityType) {
|
||||
case 'series':
|
||||
return (downloadEntity as Series).name;
|
||||
|
@ -97,6 +121,7 @@ export class DownloadService {
|
|||
case 'logs':
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,10 +142,12 @@ export class DownloadService {
|
|||
case 'volume':
|
||||
sizeCheckCall = this.downloadVolumeSize((entity as Volume).id);
|
||||
downloadCall = this.downloadVolume(entity as Volume);
|
||||
//this.enqueueDownload(entity as Volume);
|
||||
break;
|
||||
case 'chapter':
|
||||
sizeCheckCall = this.downloadChapterSize((entity as Chapter).id);
|
||||
downloadCall = this.downloadChapter(entity as Chapter);
|
||||
//this.enqueueDownload(entity as Chapter);
|
||||
break;
|
||||
case 'bookmark':
|
||||
sizeCheckCall = of(0);
|
||||
|
@ -145,8 +172,10 @@ export class DownloadService {
|
|||
})
|
||||
).pipe(filter(wantsToDownload => {
|
||||
return wantsToDownload;
|
||||
}), switchMap(() => {
|
||||
return downloadCall.pipe(
|
||||
}),
|
||||
filter(_ => downloadCall !== undefined),
|
||||
switchMap(() => {
|
||||
return (downloadCall || of(undefined)).pipe(
|
||||
tap((d) => {
|
||||
if (callback) callback(d);
|
||||
}),
|
||||
|
@ -187,7 +216,40 @@ export class DownloadService {
|
|||
);
|
||||
}
|
||||
|
||||
private getIdKey(entity: Chapter | Volume) {
|
||||
if (this.utilityService.isVolume(entity)) return 'volumeId';
|
||||
if (this.utilityService.isChapter(entity)) return 'chapterId';
|
||||
if (this.utilityService.isSeries(entity)) return 'seriesId';
|
||||
return 'id';
|
||||
}
|
||||
|
||||
private getDownloadEntityType(entity: Chapter | Volume): DownloadEntityType {
|
||||
if (this.utilityService.isVolume(entity)) return 'volume';
|
||||
if (this.utilityService.isChapter(entity)) return 'chapter';
|
||||
if (this.utilityService.isSeries(entity)) return 'series';
|
||||
return 'logs'; // This is a hack but it will never occur
|
||||
}
|
||||
|
||||
private downloadEntity<T>(entity: Chapter | Volume): Observable<any> {
|
||||
const downloadEntityType = this.getDownloadEntityType(entity);
|
||||
const subtitle = this.downloadSubtitle(downloadEntityType, entity);
|
||||
const idKey = this.getIdKey(entity);
|
||||
const url = `${this.baseUrl}download/${downloadEntityType}?${idKey}=${entity.id}`;
|
||||
|
||||
return this.httpClient.get(url, { observe: 'events', responseType: 'blob', reportProgress: true }).pipe(
|
||||
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
|
||||
download((blob, filename) => {
|
||||
this.save(blob, decodeURIComponent(filename));
|
||||
}),
|
||||
tap((d) => this.updateDownloadState(d, downloadEntityType, subtitle, entity.id)),
|
||||
finalize(() => this.finalizeDownloadState(downloadEntityType, subtitle))
|
||||
);
|
||||
}
|
||||
|
||||
private downloadSeries(series: Series) {
|
||||
|
||||
// TODO: Call backend for all the volumes and loose leaf chapters then enqueque them all
|
||||
|
||||
const downloadType = 'series';
|
||||
const subtitle = this.downloadSubtitle(downloadType, series);
|
||||
return this.httpClient.get(this.baseUrl + 'download/series?seriesId=' + series.id,
|
||||
|
@ -227,38 +289,42 @@ export class DownloadService {
|
|||
}
|
||||
|
||||
private downloadChapter(chapter: Chapter) {
|
||||
const downloadType = 'chapter';
|
||||
const subtitle = this.downloadSubtitle(downloadType, chapter);
|
||||
return this.httpClient.get(this.baseUrl + 'download/chapter?chapterId=' + chapter.id,
|
||||
{observe: 'events', responseType: 'blob', reportProgress: true}
|
||||
).pipe(
|
||||
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
|
||||
download((blob, filename) => {
|
||||
this.save(blob, decodeURIComponent(filename));
|
||||
}),
|
||||
tap((d) => this.updateDownloadState(d, downloadType, subtitle, chapter.id)),
|
||||
finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||
);
|
||||
return this.downloadEntity(chapter);
|
||||
|
||||
// const downloadType = 'chapter';
|
||||
// const subtitle = this.downloadSubtitle(downloadType, chapter);
|
||||
// return this.httpClient.get(this.baseUrl + 'download/chapter?chapterId=' + chapter.id,
|
||||
// {observe: 'events', responseType: 'blob', reportProgress: true}
|
||||
// ).pipe(
|
||||
// throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
|
||||
// download((blob, filename) => {
|
||||
// this.save(blob, decodeURIComponent(filename));
|
||||
// }),
|
||||
// tap((d) => this.updateDownloadState(d, downloadType, subtitle, chapter.id)),
|
||||
// finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||
// );
|
||||
}
|
||||
|
||||
private downloadVolume(volume: Volume): Observable<Download> {
|
||||
const downloadType = 'volume';
|
||||
const subtitle = this.downloadSubtitle(downloadType, volume);
|
||||
return this.httpClient.get(this.baseUrl + 'download/volume?volumeId=' + volume.id,
|
||||
{observe: 'events', responseType: 'blob', reportProgress: true}
|
||||
).pipe(
|
||||
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
|
||||
download((blob, filename) => {
|
||||
this.save(blob, decodeURIComponent(filename));
|
||||
}),
|
||||
tap((d) => this.updateDownloadState(d, downloadType, subtitle, volume.id)),
|
||||
finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||
);
|
||||
private downloadVolume(volume: Volume) {
|
||||
return this.downloadEntity(volume);
|
||||
// const downloadType = 'volume';
|
||||
// const subtitle = this.downloadSubtitle(downloadType, volume);
|
||||
// return this.httpClient.get(this.baseUrl + 'download/volume?volumeId=' + volume.id,
|
||||
// {observe: 'events', responseType: 'blob', reportProgress: true}
|
||||
// ).pipe(
|
||||
// throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
|
||||
// download((blob, filename) => {
|
||||
// this.save(blob, decodeURIComponent(filename));
|
||||
// }),
|
||||
// tap((d) => this.updateDownloadState(d, downloadType, subtitle, volume.id)),
|
||||
// finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||
// );
|
||||
}
|
||||
|
||||
private async confirmSize(size: number, entityType: DownloadEntityType) {
|
||||
return (size < this.SIZE_WARNING ||
|
||||
await this.confirmService.confirm(translate('toasts.confirm-download-size', {entityType: translate('entity-type.' + entityType), size: bytesPipe.transform(size)})));
|
||||
await this.confirmService.confirm(translate('toasts.confirm-download-size',
|
||||
{entityType: translate('entity-type.' + entityType), size: bytesPipe.transform(size)})));
|
||||
}
|
||||
|
||||
private downloadBookmarks(bookmarks: PageBookmark[]) {
|
||||
|
@ -276,4 +342,60 @@ export class DownloadService {
|
|||
finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private processDownload(entity: QueueableDownloadType): void {
|
||||
const downloadObservable = this.downloadEntity(entity);
|
||||
console.log('Process Download called for entity: ', entity);
|
||||
|
||||
// When we consume one, we need to take it off the queue
|
||||
|
||||
downloadObservable.subscribe((downloadEvent) => {
|
||||
// Download completed, process the next item in the queue
|
||||
if (downloadEvent.state === 'DONE') {
|
||||
this.processNextDownload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private processNextDownload(): void {
|
||||
const currentQueue = this.downloadQueue.value;
|
||||
if (currentQueue.length > 0) {
|
||||
const nextEntity = currentQueue[0];
|
||||
this.processDownload(nextEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private enqueueDownload(entity: QueueableDownloadType): void {
|
||||
const currentQueue = this.downloadQueue.value;
|
||||
const newQueue = [...currentQueue, entity];
|
||||
this.downloadQueue.next(newQueue);
|
||||
|
||||
// If the queue was empty, start processing the download
|
||||
if (currentQueue.length === 0) {
|
||||
this.processNextDownload();
|
||||
}
|
||||
}
|
||||
|
||||
mapToEntityType(events: DownloadEvent[], entity: Series | Volume | Chapter | CollectionTag | PageBookmark | RecentlyAddedItem | NextExpectedChapter) {
|
||||
if(this.utilityService.isSeries(entity)) {
|
||||
return events.find(e => e.entityType === 'series' && e.id == entity.id
|
||||
&& e.subTitle === this.downloadSubtitle('series', (entity as Series))) || null;
|
||||
}
|
||||
if(this.utilityService.isVolume(entity)) {
|
||||
return events.find(e => e.entityType === 'volume' && e.id == entity.id
|
||||
&& e.subTitle === this.downloadSubtitle('volume', (entity as Volume))) || null;
|
||||
}
|
||||
if(this.utilityService.isChapter(entity)) {
|
||||
return events.find(e => e.entityType === 'chapter' && e.id == entity.id
|
||||
&& e.subTitle === this.downloadSubtitle('chapter', (entity as Chapter))) || null;
|
||||
}
|
||||
// Is PageBookmark[]
|
||||
if(entity.hasOwnProperty('length')) {
|
||||
return events.find(e => e.entityType === 'bookmark'
|
||||
&& e.subTitle === this.downloadSubtitle('bookmark', [(entity as PageBookmark)])) || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue