From c7f0a910e08fd43ce02b0ce3a4c569b27f7bbafb Mon Sep 17 00:00:00 2001 From: ACX <8075870+acx10@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:07:57 -0700 Subject: [PATCH] refactor: split BookService into BookFileService and BookMetadataManageService (#2758) --- .../additional-file-uploader.component.ts | 6 +- .../book-browser/book-browser.component.ts | 6 +- .../book-card/book-card.component.ts | 16 +- .../book-table/book-table.component.ts | 4 +- .../lock-unlock-metadata-dialog.component.ts | 6 +- .../book-file-attacher.component.ts | 4 +- .../book-reviews/book-reviews.component.ts | 4 +- .../series-page/series-page.component.ts | 6 +- .../src/app/features/book/model/book.model.ts | 12 + .../book/service/book-file.service.ts | 252 +++++++++++ .../book/service/book-menu.service.ts | 10 +- .../service/book-metadata-manage.service.ts | 192 ++++++++ .../book/service/book-patch.service.ts | 2 +- .../app/features/book/service/book.service.ts | 420 +----------------- .../metadata-editor.component.ts | 22 +- .../metadata-picker.component.ts | 8 +- .../metadata-tabs/metadata-tabs.component.ts | 6 +- .../metadata-viewer.component.ts | 12 +- .../bulk-metadata-update-component.ts | 6 +- .../cover-search/cover-search.component.ts | 6 +- .../metadata-manager.component.ts | 8 +- .../ebook-reader/ebook-reader.component.ts | 4 +- .../global-preferences.component.ts | 6 +- 23 files changed, 543 insertions(+), 475 deletions(-) create mode 100644 booklore-ui/src/app/features/book/service/book-file.service.ts create mode 100644 booklore-ui/src/app/features/book/service/book-metadata-manage.service.ts diff --git a/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts b/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts index 0556cbf60..3305105a4 100644 --- a/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts +++ b/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts @@ -8,7 +8,7 @@ import { FileSelectEvent, FileUpload, FileUploadHandlerEvent } from 'primeng/fil import { Badge } from 'primeng/badge'; import { Tooltip } from 'primeng/tooltip'; import { Subject, takeUntil } from 'rxjs'; -import { BookService } from '../../service/book.service'; +import { BookFileService } from '../../service/book-file.service'; import { AppSettingsService } from '../../../../shared/service/app-settings.service'; import { Book, AdditionalFileType } from '../../model/book.model'; import { MessageService } from 'primeng/api'; @@ -58,7 +58,7 @@ export class AdditionalFileUploaderComponent implements OnInit, OnDestroy { constructor( private dialogRef: DynamicDialogRef, private config: DynamicDialogConfig, - private bookService: BookService, + private bookFileService: BookFileService, private appSettingsService: AppSettingsService, private messageService: MessageService, private cdr: ChangeDetectorRef @@ -152,7 +152,7 @@ export class AdditionalFileUploaderComponent implements OnInit, OnDestroy { for (const uploadFile of filesToUpload) { uploadFile.status = 'Uploading'; - this.bookService.uploadAdditionalFile( + this.bookFileService.uploadAdditionalFile( this.book.id, uploadFile.file, this.fileType, diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts index 356cb829e..da674fe42 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts @@ -3,6 +3,7 @@ import {ActivatedRoute, NavigationStart, Router} from '@angular/router'; import {ConfirmationService, MenuItem, MessageService} from 'primeng/api'; import {PageTitleService} from '../../../../shared/service/page-title.service'; import {BookService} from '../../service/book.service'; +import {BookMetadataManageService} from '../../service/book-metadata-manage.service'; import {debounceTime, filter, map, switchMap, takeUntil} from 'rxjs/operators'; import {BehaviorSubject, combineLatest, finalize, Observable, of, Subject, Subscription} from 'rxjs'; import {DynamicDialogRef} from 'primeng/dynamicdialog'; @@ -109,6 +110,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { private router = inject(Router); private messageService = inject(MessageService); private bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); private dialogHelperService = inject(BookDialogHelperService); private bookMenuService = inject(BookMenuService); private libraryShelfMenuService = inject(LibraryShelfMenuService); @@ -846,7 +848,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { severity: 'secondary' }, accept: () => { - this.bookService.regenerateCoversForBooks(Array.from(this.selectedBooks)).subscribe({ + this.bookMetadataManageService.regenerateCoversForBooks(Array.from(this.selectedBooks)).subscribe({ next: () => { this.messageService.add({ severity: 'success', @@ -886,7 +888,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { severity: 'secondary' }, accept: () => { - this.bookService.generateCustomCoversForBooks(Array.from(this.selectedBooks)).subscribe({ + this.bookMetadataManageService.generateCustomCoversForBooks(Array.from(this.selectedBooks)).subscribe({ next: () => { this.messageService.add({ severity: 'success', diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts index e86233460..246f3b4a9 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts @@ -5,6 +5,8 @@ import {Button} from 'primeng/button'; import {MenuModule} from 'primeng/menu'; import {ConfirmationService, MenuItem, MessageService} from 'primeng/api'; import {BookService} from '../../../service/book.service'; +import {BookFileService} from '../../../service/book-file.service'; +import {BookMetadataManageService} from '../../../service/book-metadata-manage.service'; import {CheckboxChangeEvent, CheckboxModule} from 'primeng/checkbox'; import {FormsModule} from '@angular/forms'; import {MetadataRefreshType} from '../../../../metadata/model/request/metadata-refresh-type.enum'; @@ -60,6 +62,8 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { private additionalFilesLoaded = false; private bookService = inject(BookService); + private bookFileService = inject(BookFileService); + private bookMetadataManageService = inject(BookMetadataManageService); private taskHelperService = inject(TaskHelperService); private userService = inject(UserService); private emailService = inject(EmailService); @@ -312,7 +316,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { label: this.t.translate('book.card.menu.download'), icon: 'pi pi-download', command: () => { - this.bookService.downloadFile(this.book); + this.bookFileService.downloadFile(this.book); } }); } else { @@ -441,7 +445,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { label: this.t.translate('book.card.menu.regenerateCover'), icon: 'pi pi-image', command: () => { - this.bookService.regenerateCover(this.book.id).subscribe({ + this.bookMetadataManageService.regenerateCover(this.book.id).subscribe({ next: () => this.messageService.add({ severity: 'success', summary: this.t.translate('common.success'), @@ -459,7 +463,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { label: this.t.translate('book.card.menu.generateCustomCover'), icon: 'pi pi-palette', command: () => { - this.bookService.generateCustomCover(this.book.id).subscribe({ + this.bookMetadataManageService.generateCustomCover(this.book.id).subscribe({ next: () => this.messageService.add({ severity: 'success', summary: this.t.translate('common.success'), @@ -617,7 +621,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { label: `${this.book.fileName || 'Book File'}`, icon: 'pi pi-file', command: () => { - this.bookService.downloadFile(this.book); + this.bookFileService.downloadFile(this.book); } }); @@ -719,7 +723,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { } private downloadAdditionalFile(book: Book, fileId: number): void { - this.bookService.downloadAdditionalFile(book, fileId); + this.bookFileService.downloadAdditionalFile(book, fileId); } private deleteAdditionalFile(bookId: number, fileId: number, fileName: string): void { @@ -731,7 +735,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { rejectIcon: 'pi pi-times', acceptButtonStyleClass: 'p-button-danger', accept: () => { - this.bookService.deleteAdditionalFile(bookId, fileId).subscribe({ + this.bookFileService.deleteAdditionalFile(bookId, fileId).subscribe({ next: () => { this.messageService.add({ severity: 'success', diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts index 8193ca1a1..694a7d580 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts @@ -9,6 +9,7 @@ import {SortOption} from '../../../model/sort.model'; import {UrlHelperService} from '../../../../../shared/service/url-helper.service'; import {Button} from 'primeng/button'; import {BookService} from '../../../service/book.service'; +import {BookMetadataManageService} from '../../../service/book-metadata-manage.service'; import {MessageService} from 'primeng/api'; import {RouterLink} from '@angular/router'; import {filter, Subject} from 'rxjs'; @@ -46,6 +47,7 @@ export class BookTableComponent implements OnInit, OnDestroy, OnChanges { protected urlHelper = inject(UrlHelperService); private bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); private messageService = inject(MessageService); private userService = inject(UserService); private datePipe = inject(DatePipe); @@ -326,7 +328,7 @@ export class BookTableComponent implements OnInit, OnDestroy, OnChanges { const allLocked = lockKeys.every(key => metadata[key] === true); const lockAction = allLocked ? 'UNLOCK' : 'LOCK'; - this.bookService.toggleAllLock(new Set([metadata.bookId]), lockAction).subscribe({ + this.bookMetadataManageService.toggleAllLock(new Set([metadata.bookId]), lockAction).subscribe({ next: () => { this.messageService.add({ severity: 'success', diff --git a/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts b/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts index eb6a3de71..8071f4a3b 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts @@ -4,7 +4,7 @@ import {FormsModule} from '@angular/forms'; import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; import {MessageService} from 'primeng/api'; -import {BookService} from '../../../service/book.service'; +import {BookMetadataManageService} from '../../../service/book-metadata-manage.service'; import {Divider} from 'primeng/divider'; import {LoadingService} from '../../../../../core/services/loading.service'; import {finalize} from 'rxjs'; @@ -23,7 +23,7 @@ import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; styleUrl: './lock-unlock-metadata-dialog.component.scss' }) export class LockUnlockMetadataDialogComponent implements OnInit { - private bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); private dynamicDialogConfig = inject(DynamicDialogConfig); dialogRef = inject(DynamicDialogRef); private messageService = inject(MessageService); @@ -129,7 +129,7 @@ export class LockUnlockMetadataDialogComponent implements OnInit { this.isSaving = true; const loader = this.loadingService.show(this.t.translate('book.lockUnlockDialog.toast.updatingFieldLocks')); - this.bookService.toggleFieldLocks(this.bookIds, fieldActions) + this.bookMetadataManageService.toggleFieldLocks(this.bookIds, fieldActions) .pipe(finalize(() => { this.isSaving = false; this.loadingService.hide(loader); diff --git a/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.ts b/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.ts index 9fb77b6a0..46f20543c 100644 --- a/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.ts +++ b/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.ts @@ -7,6 +7,7 @@ import { Checkbox } from 'primeng/checkbox'; import { Subject, takeUntil } from 'rxjs'; import { filter, take } from 'rxjs/operators'; import { BookService } from '../../service/book.service'; +import { BookFileService } from '../../service/book-file.service'; import { Book } from '../../model/book.model'; import { MessageService } from 'primeng/api'; import { TranslocoDirective, TranslocoPipe, TranslocoService } from '@jsverse/transloco'; @@ -42,6 +43,7 @@ export class BookFileAttacherComponent implements OnInit, OnDestroy { private dialogRef: DynamicDialogRef, private config: DynamicDialogConfig, private bookService: BookService, + private bookFileService: BookFileService, private messageService: MessageService ) {} @@ -131,7 +133,7 @@ export class BookFileAttacherComponent implements OnInit, OnDestroy { const sourceBookIds = this.sourceBooks.map(b => b.id); - this.bookService.attachBookFiles( + this.bookFileService.attachBookFiles( this.targetBook.id, sourceBookIds, this.deleteSourceBooks diff --git a/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts b/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts index c51ce80ce..da9bbcfb2 100644 --- a/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts +++ b/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts @@ -12,6 +12,7 @@ import {UserService} from '../../../settings/user-management/user.service'; import {FormsModule} from '@angular/forms'; import {Tooltip} from 'primeng/tooltip'; import {BookService} from '../../service/book.service'; +import {BookMetadataManageService} from '../../service/book-metadata-manage.service'; import {AppSettingsService} from '../../../../shared/service/app-settings.service'; @Component({ @@ -28,6 +29,7 @@ export class BookReviewsComponent implements OnInit, OnChanges { private reviewService = inject(BookReviewService); private bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); private confirmationService = inject(ConfirmationService); private messageService = inject(MessageService); private userService = inject(UserService); @@ -198,7 +200,7 @@ export class BookReviewsComponent implements OnInit, OnChanges { 'reviewsLocked': newLockState ? 'LOCK' : 'UNLOCK' }; - this.bookService.toggleFieldLocks([this.bookId], fieldActions) + this.bookMetadataManageService.toggleFieldLocks([this.bookId], fieldActions) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { diff --git a/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts b/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts index 26eff80e5..7fe25450d 100644 --- a/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts +++ b/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts @@ -6,6 +6,7 @@ import {filter, finalize, map, switchMap, tap} from "rxjs/operators"; import {combineLatest, Observable, Subscription} from "rxjs"; import {Book, ReadStatus} from "../../model/book.model"; import {BookService} from "../../service/book.service"; +import {BookMetadataManageService} from "../../service/book-metadata-manage.service"; import {BookCardComponent} from "../book-browser/book-card/book-card.component"; import {CoverScalePreferenceService} from "../book-browser/cover-scale-preference.service"; import {Tab, TabList, TabPanel, TabPanels, Tabs} from "primeng/tabs"; @@ -71,6 +72,7 @@ export class SeriesPageComponent implements OnDestroy { private route = inject(ActivatedRoute); private bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); protected coverScalePreferenceService = inject(CoverScalePreferenceService); private metadataCenterViewMode: "route" | "dialog" = "route"; private dialogRef?: DynamicDialogRef | null; @@ -454,7 +456,7 @@ export class SeriesPageComponent implements OnDestroy { severity: 'secondary' }, accept: () => { - this.bookService.regenerateCoversForBooks(Array.from(this.selectedBooks)).subscribe({ + this.bookMetadataManageService.regenerateCoversForBooks(Array.from(this.selectedBooks)).subscribe({ next: () => { this.messageService.add({ severity: 'success', @@ -494,7 +496,7 @@ export class SeriesPageComponent implements OnDestroy { severity: 'secondary' }, accept: () => { - this.bookService.generateCustomCoversForBooks(Array.from(this.selectedBooks)).subscribe({ + this.bookMetadataManageService.generateCustomCoversForBooks(Array.from(this.selectedBooks)).subscribe({ next: () => { this.messageService.add({ severity: 'success', diff --git a/booklore-ui/src/app/features/book/model/book.model.ts b/booklore-ui/src/app/features/book/model/book.model.ts index 3f10004b3..c762e86b6 100644 --- a/booklore-ui/src/app/features/book/model/book.model.ts +++ b/booklore-ui/src/app/features/book/model/book.model.ts @@ -412,3 +412,15 @@ export interface CreatePhysicalBookRequest { pageCount?: number; categories?: string[]; } + +export interface BookStatusUpdateResponse { + bookId: number; + readStatus: ReadStatus; + readStatusModifiedTime: string; + dateFinished?: string; +} + +export interface PersonalRatingUpdateResponse { + bookId: number; + personalRating?: number; +} diff --git a/booklore-ui/src/app/features/book/service/book-file.service.ts b/booklore-ui/src/app/features/book/service/book-file.service.ts new file mode 100644 index 000000000..afe90592e --- /dev/null +++ b/booklore-ui/src/app/features/book/service/book-file.service.ts @@ -0,0 +1,252 @@ +import {inject, Injectable} from '@angular/core'; +import {Observable, throwError} from 'rxjs'; +import {HttpClient} from '@angular/common/http'; +import {catchError, tap} from 'rxjs/operators'; +import {AdditionalFile, AdditionalFileType, Book} from '../model/book.model'; +import {API_CONFIG} from '../../../core/config/api-config'; +import {MessageService} from 'primeng/api'; +import {FileDownloadService} from '../../../shared/service/file-download.service'; +import {BookStateService} from './book-state.service'; +import {TranslocoService} from '@jsverse/transloco'; + +@Injectable({ + providedIn: 'root', +}) +export class BookFileService { + + private readonly url = `${API_CONFIG.BASE_URL}/api/v1/books`; + + private http = inject(HttpClient); + private messageService = inject(MessageService); + private fileDownloadService = inject(FileDownloadService); + private bookStateService = inject(BookStateService); + private readonly t = inject(TranslocoService); + + getFileContent(bookId: number, bookType?: string): Observable { + let url = `${this.url}/${bookId}/content`; + if (bookType) { + url += `?bookType=${bookType}`; + } + return this.http.get(url, {responseType: 'blob' as 'json'}); + } + + downloadFile(book: Book): void { + const downloadUrl = `${this.url}/${book.id}/download`; + this.fileDownloadService.downloadFile(downloadUrl, book.primaryFile?.fileName ?? 'book'); + } + + downloadAllFiles(book: Book): void { + const downloadUrl = `${this.url}/${book.id}/download-all`; + const filename = book.metadata?.title + ? `${book.metadata.title.replace(/[^a-zA-Z0-9\-_]/g, '_')}.zip` + : `book-${book.id}.zip`; + this.fileDownloadService.downloadFile(downloadUrl, filename); + } + + deleteAdditionalFile(bookId: number, fileId: number): Observable { + const deleteUrl = `${this.url}/${bookId}/files/${fileId}`; + return this.http.delete(deleteUrl).pipe( + tap(() => { + const currentState = this.bookStateService.getCurrentBookState(); + const updatedBooks = (currentState.books || []).map(book => { + if (book.id === bookId) { + return { + ...book, + alternativeFormats: book.alternativeFormats?.filter(file => file.id !== fileId), + supplementaryFiles: book.supplementaryFiles?.filter(file => file.id !== fileId) + }; + } + return book; + }); + + this.bookStateService.updateBookState({ + ...currentState, + books: updatedBooks + }); + + this.messageService.add({ + severity: 'success', + summary: this.t.translate('book.bookService.toast.fileDeletedSummary'), + detail: this.t.translate('book.bookService.toast.additionalFileDeletedDetail') + }); + }), + catchError(error => { + this.messageService.add({ + severity: 'error', + summary: this.t.translate('book.bookService.toast.fileDeleteFailedSummary'), + detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.fileDeleteFailedDetail') + }); + return throwError(() => error); + }) + ); + } + + deleteBookFile(bookId: number, fileId: number, isPrimary: boolean): Observable { + const deleteUrl = `${this.url}/${bookId}/files/${fileId}`; + return this.http.delete(deleteUrl).pipe( + tap(() => { + const currentState = this.bookStateService.getCurrentBookState(); + const updatedBooks = (currentState.books || []).map(book => { + if (book.id === bookId) { + if (isPrimary) { + const remainingAlternatives = book.alternativeFormats?.filter(file => file.id !== fileId) || []; + if (remainingAlternatives.length > 0) { + const [newPrimary, ...restAlternatives] = remainingAlternatives; + return { + ...book, + primaryFile: newPrimary, + alternativeFormats: restAlternatives + }; + } else { + return { + ...book, + primaryFile: undefined, + alternativeFormats: [] + }; + } + } else { + return { + ...book, + alternativeFormats: book.alternativeFormats?.filter(file => file.id !== fileId) + }; + } + } + return book; + }); + + this.bookStateService.updateBookState({ + ...currentState, + books: updatedBooks + }); + + this.messageService.add({ + severity: 'success', + summary: this.t.translate('book.bookService.toast.fileDeletedSummary'), + detail: this.t.translate('book.bookService.toast.bookFileDeletedDetail') + }); + }), + catchError(error => { + this.messageService.add({ + severity: 'error', + summary: this.t.translate('book.bookService.toast.fileDeleteFailedSummary'), + detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.fileDeleteFailedDetail') + }); + return throwError(() => error); + }) + ); + } + + uploadAdditionalFile(bookId: number, file: File, fileType: AdditionalFileType, description?: string): Observable { + const formData = new FormData(); + formData.append('file', file); + + const isBook = fileType === AdditionalFileType.ALTERNATIVE_FORMAT; + formData.append('isBook', String(isBook)); + + if (isBook) { + const lower = (file?.name || '').toLowerCase(); + const ext = lower.includes('.') ? lower.substring(lower.lastIndexOf('.') + 1) : ''; + const bookType = ext === 'pdf' + ? 'PDF' + : ext === 'epub' + ? 'EPUB' + : (ext === 'cbz' || ext === 'cbr' || ext === 'cb7' || ext === 'cbt') + ? 'CBX' + : (ext === 'm4b' || ext === 'm4a' || ext === 'mp3') + ? 'AUDIOBOOK' + : null; + + if (bookType) { + formData.append('bookType', bookType); + } + } + if (description) { + formData.append('description', description); + } + + return this.http.post(`${this.url}/${bookId}/files`, formData).pipe( + tap((newFile) => { + const currentState = this.bookStateService.getCurrentBookState(); + const updatedBooks = (currentState.books || []).map(book => { + if (book.id === bookId) { + const updatedBook = {...book}; + if (fileType === AdditionalFileType.ALTERNATIVE_FORMAT) { + updatedBook.alternativeFormats = [...(book.alternativeFormats || []), newFile]; + } else { + updatedBook.supplementaryFiles = [...(book.supplementaryFiles || []), newFile]; + } + return updatedBook; + } + return book; + }); + + this.bookStateService.updateBookState({ + ...currentState, + books: updatedBooks + }); + + this.messageService.add({ + severity: 'success', + summary: this.t.translate('book.bookService.toast.fileUploadedSummary'), + detail: this.t.translate('book.bookService.toast.fileUploadedDetail') + }); + }), + catchError(error => { + this.messageService.add({ + severity: 'error', + summary: this.t.translate('book.bookService.toast.uploadFailedSummary'), + detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.uploadFailedDetail') + }); + return throwError(() => error); + }) + ); + } + + downloadAdditionalFile(book: Book, fileId: number): void { + const additionalFile = [ + ...(book.alternativeFormats || []), + ...(book.supplementaryFiles || []) + ].find((f: AdditionalFile) => f.id === fileId); + const downloadUrl = `${this.url}/${book.id}/files/${fileId}/download`; + this.fileDownloadService.downloadFile(downloadUrl, additionalFile?.fileName ?? 'file'); + } + + attachBookFiles(targetBookId: number, sourceBookIds: number[], deleteSourceBooks: boolean): Observable { + return this.http.post(`${this.url}/${targetBookId}/attach-file`, { + sourceBookIds, + deleteSourceBooks + }).pipe( + tap(updatedBook => { + const currentState = this.bookStateService.getCurrentBookState(); + let updatedBooks = (currentState.books || []).map(book => + book.id === targetBookId ? updatedBook : book + ); + + if (deleteSourceBooks) { + const sourceIdSet = new Set(sourceBookIds); + updatedBooks = updatedBooks.filter(book => !sourceIdSet.has(book.id)); + } + + this.bookStateService.updateBookState({ + ...currentState, + books: updatedBooks + }); + + const fileCount = sourceBookIds.length; + this.messageService.add({ + severity: 'success', + summary: this.t.translate('book.bookService.toast.filesAttachedSummary'), + detail: this.t.translate('book.bookService.toast.filesAttachedDetail', {count: fileCount}) + }); + }), + catchError(error => { + this.messageService.add({ + severity: 'error', + summary: this.t.translate('book.bookService.toast.attachmentFailedSummary'), + detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.attachmentFailedDetail') + }); + return throwError(() => error); + }) + ); + } +} diff --git a/booklore-ui/src/app/features/book/service/book-menu.service.ts b/booklore-ui/src/app/features/book/service/book-menu.service.ts index 6b8eb3ce7..e4108b614 100644 --- a/booklore-ui/src/app/features/book/service/book-menu.service.ts +++ b/booklore-ui/src/app/features/book/service/book-menu.service.ts @@ -1,6 +1,7 @@ import {inject, Injectable} from '@angular/core'; import {ConfirmationService, MenuItem, MessageService} from 'primeng/api'; import {BookService} from './book.service'; +import {BookMetadataManageService} from './book-metadata-manage.service'; import {AGE_RATING_OPTIONS, CONTENT_RATING_LABELS, readStatusLabels} from '../components/book-browser/book-filter/book-filter.config'; import {ReadStatus} from '../model/book.model'; import {ResetProgressTypes} from '../../../shared/constants/reset-progress-type'; @@ -19,6 +20,7 @@ export class BookMenuService { confirmationService = inject(ConfirmationService); messageService = inject(MessageService); bookService = inject(BookService); + bookMetadataManageService = inject(BookMetadataManageService); loadingService = inject(LoadingService); private readonly t = inject(TranslocoService); @@ -152,7 +154,7 @@ export class BookMenuService { rejectLabel: this.t.translate('common.no'), accept: () => { const loader = this.loadingService.show(this.t.translate('book.menuService.loading.settingAgeRating', {count})); - this.bookService.updateBooksMetadata({ + this.bookMetadataManageService.updateBooksMetadata({ bookIds: Array.from(selectedBooks), ageRating: option.id }).pipe(finalize(() => this.loadingService.hide(loader))) @@ -194,7 +196,7 @@ export class BookMenuService { rejectLabel: this.t.translate('common.no'), accept: () => { const loader = this.loadingService.show(this.t.translate('book.menuService.loading.clearingAgeRating', {count})); - this.bookService.updateBooksMetadata({ + this.bookMetadataManageService.updateBooksMetadata({ bookIds: Array.from(selectedBooks), clearAgeRating: true }).pipe(finalize(() => this.loadingService.hide(loader))) @@ -239,7 +241,7 @@ export class BookMenuService { rejectLabel: this.t.translate('common.no'), accept: () => { const loader = this.loadingService.show(this.t.translate('book.menuService.loading.settingContentRating', {count})); - this.bookService.updateBooksMetadata({ + this.bookMetadataManageService.updateBooksMetadata({ bookIds: Array.from(selectedBooks), contentRating: value }).pipe(finalize(() => this.loadingService.hide(loader))) @@ -281,7 +283,7 @@ export class BookMenuService { rejectLabel: this.t.translate('common.no'), accept: () => { const loader = this.loadingService.show(this.t.translate('book.menuService.loading.clearingContentRating', {count})); - this.bookService.updateBooksMetadata({ + this.bookMetadataManageService.updateBooksMetadata({ bookIds: Array.from(selectedBooks), clearContentRating: true }).pipe(finalize(() => this.loadingService.hide(loader))) diff --git a/booklore-ui/src/app/features/book/service/book-metadata-manage.service.ts b/booklore-ui/src/app/features/book/service/book-metadata-manage.service.ts new file mode 100644 index 000000000..cd701e3ca --- /dev/null +++ b/booklore-ui/src/app/features/book/service/book-metadata-manage.service.ts @@ -0,0 +1,192 @@ +import {inject, Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {HttpClient, HttpParams} from '@angular/common/http'; +import {catchError, map, tap} from 'rxjs/operators'; +import {Book, BookMetadata, BulkMetadataUpdateRequest, MetadataUpdateWrapper} from '../model/book.model'; +import {API_CONFIG} from '../../../core/config/api-config'; +import {MessageService} from 'primeng/api'; +import {BookStateService} from './book-state.service'; +import {BookSocketService} from './book-socket.service'; +import {TranslocoService} from '@jsverse/transloco'; +import {BookService} from './book.service'; + +@Injectable({ + providedIn: 'root', +}) +export class BookMetadataManageService { + + private readonly url = `${API_CONFIG.BASE_URL}/api/v1/books`; + + private http = inject(HttpClient); + private messageService = inject(MessageService); + private bookStateService = inject(BookStateService); + private bookSocketService = inject(BookSocketService); + private bookService = inject(BookService); + private readonly t = inject(TranslocoService); + + updateBookMetadata(bookId: number | undefined, wrapper: MetadataUpdateWrapper, mergeCategories: boolean): Observable { + const params = new HttpParams().set('mergeCategories', mergeCategories.toString()); + return this.http.put(`${this.url}/${bookId}/metadata`, wrapper, {params}).pipe( + map(updatedMetadata => { + this.bookSocketService.handleBookMetadataUpdate(bookId!, updatedMetadata); + return updatedMetadata; + }) + ); + } + + updateBooksMetadata(request: BulkMetadataUpdateRequest): Observable { + return this.http.put(`${this.url}/bulk-edit-metadata`, request).pipe( + map(() => void 0) + ); + } + + toggleAllLock(bookIds: Set, lock: string): Observable { + const requestBody = { + bookIds: Array.from(bookIds), + lock: lock + }; + return this.http.put(`${this.url}/metadata/toggle-all-lock`, requestBody).pipe( + tap((updatedMetadataList) => { + const currentState = this.bookStateService.getCurrentBookState(); + const updatedBooks = (currentState.books || []).map(book => { + const updatedMetadata = updatedMetadataList.find(meta => meta.bookId === book.id); + return updatedMetadata ? {...book, metadata: updatedMetadata} : book; + }); + this.bookStateService.updateBookState({...currentState, books: updatedBooks}); + }), + map(() => void 0), + catchError((error) => { + throw error; + }) + ); + } + + toggleFieldLocks(bookIds: number[] | Set, fieldActions: Record): Observable { + const bookIdSet = bookIds instanceof Set ? bookIds : new Set(bookIds); + + const requestBody = { + bookIds: Array.from(bookIdSet), + fieldActions + }; + + return this.http.put(`${this.url}/metadata/toggle-field-locks`, requestBody).pipe( + tap(() => { + const currentState = this.bookStateService.getCurrentBookState(); + const updatedBooks = (currentState.books || []).map(book => { + if (!bookIdSet.has(book.id)) return book; + const updatedMetadata = {...book.metadata}; + for (const [field, action] of Object.entries(fieldActions)) { + const lockField = field.endsWith('Locked') ? field : `${field}Locked`; + if (lockField in updatedMetadata) { + (updatedMetadata as Record)[lockField] = action === 'LOCK'; + } + } + return { + ...book, + metadata: updatedMetadata + }; + }); + this.bookStateService.updateBookState({ + ...currentState, + books: updatedBooks as Book[] + }); + }), + catchError(error => { + this.messageService.add({ + severity: 'error', + summary: this.t.translate('book.bookService.toast.fieldLockFailedSummary'), + detail: this.t.translate('book.bookService.toast.fieldLockFailedDetail'), + }); + throw error; + }) + ); + } + + consolidateMetadata(metadataType: 'authors' | 'categories' | 'moods' | 'tags' | 'series' | 'publishers' | 'languages', targetValues: string[], valuesToMerge: string[]): Observable { + const payload = {metadataType, targetValues, valuesToMerge}; + return this.http.post(`${this.url}/metadata/manage/consolidate`, payload).pipe( + tap(() => { + this.bookService.refreshBooks(); + }) + ); + } + + deleteMetadata(metadataType: 'authors' | 'categories' | 'moods' | 'tags' | 'series' | 'publishers' | 'languages', valuesToDelete: string[]): Observable { + const payload = {metadataType, valuesToDelete}; + return this.http.post(`${this.url}/metadata/manage/delete`, payload).pipe( + tap(() => { + this.bookService.refreshBooks(); + }) + ); + } + + /*------------------ Cover Operations ------------------*/ + + getUploadCoverUrl(bookId: number): string { + return this.url + '/' + bookId + "/metadata/cover/upload" + } + + uploadCoverFromUrl(bookId: number, url: string): Observable { + return this.http.post(`${this.url}/${bookId}/metadata/cover/from-url`, {url}); + } + + regenerateCovers(): Observable { + return this.http.post(`${this.url}/regenerate-covers`, {}); + } + + regenerateCover(bookId: number): Observable { + return this.http.post(`${this.url}/${bookId}/regenerate-cover`, {}); + } + + getFileMetadata(bookId: number): Observable { + return this.http.get(`${this.url}/${bookId}/file-metadata`); + } + + generateCustomCover(bookId: number): Observable { + return this.http.post(`${this.url}/${bookId}/generate-custom-cover`, {}); + } + + generateCustomCoversForBooks(bookIds: number[]): Observable { + return this.http.post(`${this.url}/bulk-generate-custom-covers`, {bookIds}); + } + + regenerateCoversForBooks(bookIds: number[]): Observable { + return this.http.post(`${this.url}/bulk-regenerate-covers`, {bookIds}); + } + + uploadAudiobookCoverFromUrl(bookId: number, url: string): Observable { + return this.http.post(`${this.url}/${bookId}/metadata/audiobook-cover/from-url`, {url}); + } + + uploadAudiobookCoverFromFile(bookId: number, file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + return this.http.post(`${this.url}/${bookId}/metadata/audiobook-cover/upload`, formData); + } + + getUploadAudiobookCoverUrl(bookId: number): string { + return this.url + '/' + bookId + "/metadata/audiobook-cover/upload"; + } + + regenerateAudiobookCover(bookId: number): Observable { + return this.http.post(`${this.url}/${bookId}/regenerate-audiobook-cover`, {}); + } + + generateCustomAudiobookCover(bookId: number): Observable { + return this.http.post(`${this.url}/${bookId}/generate-custom-audiobook-cover`, {}); + } + + supportsDualCovers(book: Book): boolean { + const allFiles = [book.primaryFile, ...(book.alternativeFormats || [])].filter(f => f?.bookType); + const hasAudiobook = allFiles.some(f => f!.bookType === 'AUDIOBOOK'); + const hasEbook = allFiles.some(f => f!.bookType !== 'AUDIOBOOK'); + return hasAudiobook && hasEbook; + } + + bulkUploadCover(bookIds: number[], file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + formData.append('bookIds', bookIds.join(',')); + return this.http.post(`${this.url}/bulk-upload-cover`, formData); + } +} diff --git a/booklore-ui/src/app/features/book/service/book-patch.service.ts b/booklore-ui/src/app/features/book/service/book-patch.service.ts index 0db365550..899718a13 100644 --- a/booklore-ui/src/app/features/book/service/book-patch.service.ts +++ b/booklore-ui/src/app/features/book/service/book-patch.service.ts @@ -6,7 +6,7 @@ import {Book, BookFileProgress, ReadStatus} from '../model/book.model'; import {BookStateService} from './book-state.service'; import {API_CONFIG} from '../../../core/config/api-config'; import {ResetProgressType, ResetProgressTypes} from '../../../shared/constants/reset-progress-type'; -import {BookStatusUpdateResponse, PersonalRatingUpdateResponse} from './book.service'; +import {BookStatusUpdateResponse, PersonalRatingUpdateResponse} from '../model/book.model'; @Injectable({ providedIn: 'root', diff --git a/booklore-ui/src/app/features/book/service/book.service.ts b/booklore-ui/src/app/features/book/service/book.service.ts index f9082f57f..cb19a63bf 100644 --- a/booklore-ui/src/app/features/book/service/book.service.ts +++ b/booklore-ui/src/app/features/book/service/book.service.ts @@ -2,13 +2,12 @@ import {inject, Injectable} from '@angular/core'; import {first, from, Observable, of, throwError} from 'rxjs'; import {HttpClient, HttpParams} from '@angular/common/http'; import {catchError, distinctUntilChanged, filter, finalize, map, shareReplay, switchMap, tap} from 'rxjs/operators'; -import {AdditionalFile, AdditionalFileType, Book, BookDeletionResponse, BookMetadata, BookRecommendation, BookSetting, BookSyncResponse, BookType, BulkMetadataUpdateRequest, CreatePhysicalBookRequest, MetadataUpdateWrapper, ReadStatus} from '../model/book.model'; +import {Book, BookDeletionResponse, BookMetadata, BookRecommendation, BookSetting, BookStatusUpdateResponse, BookSyncResponse, BookType, CreatePhysicalBookRequest, PersonalRatingUpdateResponse, ReadStatus} from '../model/book.model'; import {BookState} from '../model/state/book-state.model'; import {API_CONFIG} from '../../../core/config/api-config'; import {MessageService} from 'primeng/api'; import {ResetProgressType} from '../../../shared/constants/reset-progress-type'; import {AuthService} from '../../../shared/service/auth.service'; -import {FileDownloadService} from '../../../shared/service/file-download.service'; import {Router} from '@angular/router'; import {BookStateService} from './book-state.service'; import {BookSocketService} from './book-socket.service'; @@ -16,18 +15,6 @@ import {BookPatchService} from './book-patch.service'; import {BookCacheService} from './book-cache.service'; import {TranslocoService} from '@jsverse/transloco'; -export interface BookStatusUpdateResponse { - bookId: number; - readStatus: ReadStatus; - readStatusModifiedTime: string; - dateFinished?: string; -} - -export interface PersonalRatingUpdateResponse { - bookId: number; - personalRating?: number; -} - @Injectable({ providedIn: 'root', }) @@ -38,7 +25,6 @@ export class BookService { private http = inject(HttpClient); private messageService = inject(MessageService); private authService = inject(AuthService); - private fileDownloadService = inject(FileDownloadService); private router = inject(Router); private bookStateService = inject(BookStateService); private bookSocketService = inject(BookSocketService); @@ -449,199 +435,6 @@ export class BookService { return this.http.put(`${this.url}/${bookId}/viewer-setting`, bookSetting); } - /*------------------ File Operations ------------------*/ - - getFileContent(bookId: number, bookType?: string): Observable { - let url = `${this.url}/${bookId}/content`; - if (bookType) { - url += `?bookType=${bookType}`; - } - return this.http.get(url, {responseType: 'blob' as 'json'}); - } - - downloadFile(book: Book): void { - const downloadUrl = `${this.url}/${book.id}/download`; - this.fileDownloadService.downloadFile(downloadUrl, book.primaryFile?.fileName ?? 'book'); - } - - downloadAllFiles(book: Book): void { - const downloadUrl = `${this.url}/${book.id}/download-all`; - const filename = book.metadata?.title - ? `${book.metadata.title.replace(/[^a-zA-Z0-9\-_]/g, '_')}.zip` - : `book-${book.id}.zip`; - this.fileDownloadService.downloadFile(downloadUrl, filename); - } - - deleteAdditionalFile(bookId: number, fileId: number): Observable { - const deleteUrl = `${this.url}/${bookId}/files/${fileId}`; - return this.http.delete(deleteUrl).pipe( - tap(() => { - const currentState = this.bookStateService.getCurrentBookState(); - const updatedBooks = (currentState.books || []).map(book => { - if (book.id === bookId) { - return { - ...book, - alternativeFormats: book.alternativeFormats?.filter(file => file.id !== fileId), - supplementaryFiles: book.supplementaryFiles?.filter(file => file.id !== fileId) - }; - } - return book; - }); - - this.bookStateService.updateBookState({ - ...currentState, - books: updatedBooks - }); - - this.messageService.add({ - severity: 'success', - summary: this.t.translate('book.bookService.toast.fileDeletedSummary'), - detail: this.t.translate('book.bookService.toast.additionalFileDeletedDetail') - }); - }), - catchError(error => { - this.messageService.add({ - severity: 'error', - summary: this.t.translate('book.bookService.toast.fileDeleteFailedSummary'), - detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.fileDeleteFailedDetail') - }); - return throwError(() => error); - }) - ); - } - - deleteBookFile(bookId: number, fileId: number, isPrimary: boolean): Observable { - const deleteUrl = `${this.url}/${bookId}/files/${fileId}`; - return this.http.delete(deleteUrl).pipe( - tap(() => { - const currentState = this.bookStateService.getCurrentBookState(); - const updatedBooks = (currentState.books || []).map(book => { - if (book.id === bookId) { - if (isPrimary) { - // Primary file was deleted - promote first alternative to primary, or set null - const remainingAlternatives = book.alternativeFormats?.filter(file => file.id !== fileId) || []; - if (remainingAlternatives.length > 0) { - const [newPrimary, ...restAlternatives] = remainingAlternatives; - return { - ...book, - primaryFile: newPrimary, - alternativeFormats: restAlternatives - }; - } else { - return { - ...book, - primaryFile: undefined, - alternativeFormats: [] - }; - } - } else { - // Alternative file was deleted - return { - ...book, - alternativeFormats: book.alternativeFormats?.filter(file => file.id !== fileId) - }; - } - } - return book; - }); - - this.bookStateService.updateBookState({ - ...currentState, - books: updatedBooks - }); - - this.messageService.add({ - severity: 'success', - summary: this.t.translate('book.bookService.toast.fileDeletedSummary'), - detail: this.t.translate('book.bookService.toast.bookFileDeletedDetail') - }); - }), - catchError(error => { - this.messageService.add({ - severity: 'error', - summary: this.t.translate('book.bookService.toast.fileDeleteFailedSummary'), - detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.fileDeleteFailedDetail') - }); - return throwError(() => error); - }) - ); - } - - uploadAdditionalFile(bookId: number, file: File, fileType: AdditionalFileType, description?: string): Observable { - const formData = new FormData(); - formData.append('file', file); - - const isBook = fileType === AdditionalFileType.ALTERNATIVE_FORMAT; - formData.append('isBook', String(isBook)); - - if (isBook) { - const lower = (file?.name || '').toLowerCase(); - const ext = lower.includes('.') ? lower.substring(lower.lastIndexOf('.') + 1) : ''; - const bookType = ext === 'pdf' - ? 'PDF' - : ext === 'epub' - ? 'EPUB' - : (ext === 'cbz' || ext === 'cbr' || ext === 'cb7' || ext === 'cbt') - ? 'CBX' - : (ext === 'm4b' || ext === 'm4a' || ext === 'mp3') - ? 'AUDIOBOOK' - : null; - - if (bookType) { - formData.append('bookType', bookType); - } - } - if (description) { - formData.append('description', description); - } - - return this.http.post(`${this.url}/${bookId}/files`, formData).pipe( - tap((newFile) => { - const currentState = this.bookStateService.getCurrentBookState(); - const updatedBooks = (currentState.books || []).map(book => { - if (book.id === bookId) { - const updatedBook = {...book}; - if (fileType === AdditionalFileType.ALTERNATIVE_FORMAT) { - updatedBook.alternativeFormats = [...(book.alternativeFormats || []), newFile]; - } else { - updatedBook.supplementaryFiles = [...(book.supplementaryFiles || []), newFile]; - } - return updatedBook; - } - return book; - }); - - this.bookStateService.updateBookState({ - ...currentState, - books: updatedBooks - }); - - this.messageService.add({ - severity: 'success', - summary: this.t.translate('book.bookService.toast.fileUploadedSummary'), - detail: this.t.translate('book.bookService.toast.fileUploadedDetail') - }); - }), - catchError(error => { - this.messageService.add({ - severity: 'error', - summary: this.t.translate('book.bookService.toast.uploadFailedSummary'), - detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.uploadFailedDetail') - }); - return throwError(() => error); - }) - ); - } - - downloadAdditionalFile(book: Book, fileId: number): void { - const additionalFile = [ - ...(book.alternativeFormats || []), - ...(book.supplementaryFiles || []) - ].find((f: AdditionalFile) => f.id === fileId); - const downloadUrl = `${this.url}/${book.id}/files/${fileId}/download`; - this.fileDownloadService.downloadFile(downloadUrl, additionalFile?.fileName ?? 'file'); - } - /*------------------ Progress & Status Tracking ------------------*/ updateLastReadTime(bookId: number): void { @@ -682,175 +475,6 @@ export class BookService { return this.bookPatchService.updatePersonalRating(bookIds, rating); } - /*------------------ Metadata Operations ------------------*/ - - - updateBookMetadata(bookId: number | undefined, wrapper: MetadataUpdateWrapper, mergeCategories: boolean): Observable { - const params = new HttpParams().set('mergeCategories', mergeCategories.toString()); - return this.http.put(`${this.url}/${bookId}/metadata`, wrapper, {params}).pipe( - map(updatedMetadata => { - this.handleBookMetadataUpdate(bookId!, updatedMetadata); - return updatedMetadata; - }) - ); - } - - updateBooksMetadata(request: BulkMetadataUpdateRequest): Observable { - return this.http.put(`${this.url}/bulk-edit-metadata`, request).pipe( - map(() => void 0) - ); - } - - toggleAllLock(bookIds: Set, lock: string): Observable { - const requestBody = { - bookIds: Array.from(bookIds), - lock: lock - }; - return this.http.put(`${this.url}/metadata/toggle-all-lock`, requestBody).pipe( - tap((updatedMetadataList) => { - const currentState = this.bookStateService.getCurrentBookState(); - const updatedBooks = (currentState.books || []).map(book => { - const updatedMetadata = updatedMetadataList.find(meta => meta.bookId === book.id); - return updatedMetadata ? {...book, metadata: updatedMetadata} : book; - }); - this.bookStateService.updateBookState({...currentState, books: updatedBooks}); - }), - map(() => void 0), - catchError((error) => { - throw error; - }) - ); - } - - toggleFieldLocks(bookIds: number[] | Set, fieldActions: Record): Observable { - const bookIdSet = bookIds instanceof Set ? bookIds : new Set(bookIds); - - const requestBody = { - bookIds: Array.from(bookIdSet), - fieldActions - }; - - return this.http.put(`${this.url}/metadata/toggle-field-locks`, requestBody).pipe( - tap(() => { - const currentState = this.bookStateService.getCurrentBookState(); - const updatedBooks = (currentState.books || []).map(book => { - if (!bookIdSet.has(book.id)) return book; - const updatedMetadata = {...book.metadata}; - for (const [field, action] of Object.entries(fieldActions)) { - const lockField = field.endsWith('Locked') ? field : `${field}Locked`; - if (lockField in updatedMetadata) { - (updatedMetadata as Record)[lockField] = action === 'LOCK'; - } - } - return { - ...book, - metadata: updatedMetadata - }; - }); - this.bookStateService.updateBookState({ - ...currentState, - books: updatedBooks as Book[] - }); - }), - catchError(error => { - this.messageService.add({ - severity: 'error', - summary: this.t.translate('book.bookService.toast.fieldLockFailedSummary'), - detail: this.t.translate('book.bookService.toast.fieldLockFailedDetail'), - }); - throw error; - }) - ); - } - - consolidateMetadata(metadataType: 'authors' | 'categories' | 'moods' | 'tags' | 'series' | 'publishers' | 'languages', targetValues: string[], valuesToMerge: string[]): Observable { - const payload = {metadataType, targetValues, valuesToMerge}; - return this.http.post(`${this.url}/metadata/manage/consolidate`, payload).pipe( - tap(() => { - this.refreshBooks(); - }) - ); - } - - deleteMetadata(metadataType: 'authors' | 'categories' | 'moods' | 'tags' | 'series' | 'publishers' | 'languages', valuesToDelete: string[]): Observable { - const payload = {metadataType, valuesToDelete}; - return this.http.post(`${this.url}/metadata/manage/delete`, payload).pipe( - tap(() => { - this.refreshBooks(); - }) - ); - } - - /*------------------ Cover Operations ------------------*/ - - getUploadCoverUrl(bookId: number): string { - return this.url + '/' + bookId + "/metadata/cover/upload" - } - - uploadCoverFromUrl(bookId: number, url: string): Observable { - return this.http.post(`${this.url}/${bookId}/metadata/cover/from-url`, {url}); - } - - regenerateCovers(): Observable { - return this.http.post(`${this.url}/regenerate-covers`, {}); - } - - regenerateCover(bookId: number): Observable { - return this.http.post(`${this.url}/${bookId}/regenerate-cover`, {}); - } - - getFileMetadata(bookId: number): Observable { - return this.http.get(`${this.url}/${bookId}/file-metadata`); - } - - generateCustomCover(bookId: number): Observable { - return this.http.post(`${this.url}/${bookId}/generate-custom-cover`, {}); - } - - generateCustomCoversForBooks(bookIds: number[]): Observable { - return this.http.post(`${this.url}/bulk-generate-custom-covers`, {bookIds}); - } - - regenerateCoversForBooks(bookIds: number[]): Observable { - return this.http.post(`${this.url}/bulk-regenerate-covers`, {bookIds}); - } - - uploadAudiobookCoverFromUrl(bookId: number, url: string): Observable { - return this.http.post(`${this.url}/${bookId}/metadata/audiobook-cover/from-url`, {url}); - } - - uploadAudiobookCoverFromFile(bookId: number, file: File): Observable { - const formData = new FormData(); - formData.append('file', file); - return this.http.post(`${this.url}/${bookId}/metadata/audiobook-cover/upload`, formData); - } - - getUploadAudiobookCoverUrl(bookId: number): string { - return this.url + '/' + bookId + "/metadata/audiobook-cover/upload"; - } - - regenerateAudiobookCover(bookId: number): Observable { - return this.http.post(`${this.url}/${bookId}/regenerate-audiobook-cover`, {}); - } - - generateCustomAudiobookCover(bookId: number): Observable { - return this.http.post(`${this.url}/${bookId}/generate-custom-audiobook-cover`, {}); - } - - supportsDualCovers(book: Book): boolean { - const allFiles = [book.primaryFile, ...(book.alternativeFormats || [])].filter(f => f?.bookType); - const hasAudiobook = allFiles.some(f => f!.bookType === 'AUDIOBOOK'); - const hasEbook = allFiles.some(f => f!.bookType !== 'AUDIOBOOK'); - return hasAudiobook && hasEbook; - } - - bulkUploadCover(bookIds: number[], file: File): Observable { - const formData = new FormData(); - formData.append('file', file); - formData.append('bookIds', bookIds.join(',')); - return this.http.post(`${this.url}/bulk-upload-cover`, formData); - } - /*------------------ Websocket Handlers ------------------*/ handleNewlyCreatedBook(book: Book): void { @@ -876,46 +500,4 @@ export class BookService { handleMultipleBookCoverPatches(patches: { id: number; coverUpdatedOn: string }[]): void { this.bookSocketService.handleMultipleBookCoverPatches(patches); } - - /*------------------ Book File Attachment ------------------*/ - - attachBookFiles(targetBookId: number, sourceBookIds: number[], deleteSourceBooks: boolean): Observable { - return this.http.post(`${this.url}/${targetBookId}/attach-file`, { - sourceBookIds, - deleteSourceBooks - }).pipe( - tap(updatedBook => { - const currentState = this.bookStateService.getCurrentBookState(); - let updatedBooks = (currentState.books || []).map(book => - book.id === targetBookId ? updatedBook : book - ); - - // If deleteSourceBooks, remove source books from state - if (deleteSourceBooks) { - const sourceIdSet = new Set(sourceBookIds); - updatedBooks = updatedBooks.filter(book => !sourceIdSet.has(book.id)); - } - - this.bookStateService.updateBookState({ - ...currentState, - books: updatedBooks - }); - - const fileCount = sourceBookIds.length; - this.messageService.add({ - severity: 'success', - summary: this.t.translate('book.bookService.toast.filesAttachedSummary'), - detail: this.t.translate('book.bookService.toast.filesAttachedDetail', {count: fileCount}) - }); - }), - catchError(error => { - this.messageService.add({ - severity: 'error', - summary: this.t.translate('book.bookService.toast.attachmentFailedSummary'), - detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.attachmentFailedDetail') - }); - return throwError(() => error); - }) - ); - } } diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts index c217fbc2b..42f400613 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts @@ -12,6 +12,7 @@ import {ALL_COMIC_METADATA_FIELDS, AUDIOBOOK_METADATA_FIELDS, COMIC_FORM_TO_MODE import {FileUpload, FileUploadErrorEvent, FileUploadEvent,} from "primeng/fileupload"; import {HttpResponse} from "@angular/common/http"; import {BookService} from "../../../../book/service/book.service"; +import {BookMetadataManageService} from "../../../../book/service/book-metadata-manage.service"; import {ProgressSpinner} from "primeng/progressspinner"; import {Tooltip} from "primeng/tooltip"; import {filter, finalize, switchMap, take, tap} from "rxjs/operators"; @@ -69,6 +70,7 @@ export class MetadataEditorComponent implements OnInit { private messageService = inject(MessageService); private bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); private taskHelperService = inject(TaskHelperService); protected urlHelper = inject(UrlHelperService); private bookDialogHelperService = inject(BookDialogHelperService); @@ -595,7 +597,7 @@ export class MetadataEditorComponent implements OnInit { saveMetadata(): Observable { this.isSaving = true; - return this.bookService + return this.bookMetadataManageService .updateBookMetadata( this.currentBookId, this.buildMetadataWrapper(undefined), @@ -853,7 +855,7 @@ export class MetadataEditorComponent implements OnInit { private updateMetadata(shouldLockAllFields: boolean | undefined): void { const metadataUpdateWrapper = this.buildMetadataWrapper(shouldLockAllFields); - this.bookService + this.bookMetadataManageService .updateBookMetadata(this.currentBookId, metadataUpdateWrapper, false) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ @@ -881,7 +883,7 @@ export class MetadataEditorComponent implements OnInit { } getUploadCoverUrl(): string { - return this.bookService.getUploadCoverUrl(this.currentBookId); + return this.bookMetadataManageService.getUploadCoverUrl(this.currentBookId); } onBeforeSend(): void { @@ -915,7 +917,7 @@ export class MetadataEditorComponent implements OnInit { } regenerateCover(bookId: number) { - this.bookService.regenerateCover(bookId).pipe( + this.bookMetadataManageService.regenerateCover(bookId).pipe( switchMap(() => this.bookService.getBookByIdFromAPI(bookId, false)), takeUntilDestroyed(this.destroyRef) ).subscribe({ @@ -939,7 +941,7 @@ export class MetadataEditorComponent implements OnInit { generateCustomCover(bookId: number) { this.isGeneratingCover = true; - this.bookService.generateCustomCover(bookId).pipe( + this.bookMetadataManageService.generateCustomCover(bookId).pipe( switchMap(() => this.bookService.getBookByIdFromAPI(bookId, false)), finalize(() => this.isGeneratingCover = false), takeUntilDestroyed(this.destroyRef) @@ -963,7 +965,7 @@ export class MetadataEditorComponent implements OnInit { } regenerateAudiobookCover(bookId: number) { - this.bookService.regenerateAudiobookCover(bookId).pipe( + this.bookMetadataManageService.regenerateAudiobookCover(bookId).pipe( switchMap(() => this.bookService.getBookByIdFromAPI(bookId, false)), takeUntilDestroyed(this.destroyRef) ).subscribe({ @@ -987,7 +989,7 @@ export class MetadataEditorComponent implements OnInit { generateCustomAudiobookCover(bookId: number) { this.isGeneratingAudiobookCover = true; - this.bookService.generateCustomAudiobookCover(bookId).pipe( + this.bookMetadataManageService.generateCustomAudiobookCover(bookId).pipe( switchMap(() => this.bookService.getBookByIdFromAPI(bookId, false)), finalize(() => this.isGeneratingAudiobookCover = false), takeUntilDestroyed(this.destroyRef) @@ -1038,7 +1040,7 @@ export class MetadataEditorComponent implements OnInit { fetchFromFile(bookId: number) { this.isFetchingFromFile = true; - this.bookService.getFileMetadata(bookId).pipe( + this.bookMetadataManageService.getFileMetadata(bookId).pipe( finalize(() => this.isFetchingFromFile = false), takeUntilDestroyed(this.destroyRef) ).subscribe({ @@ -1156,7 +1158,7 @@ export class MetadataEditorComponent implements OnInit { } supportsDualCovers(book: Book): boolean { - return this.bookService.supportsDualCovers(book); + return this.bookMetadataManageService.supportsDualCovers(book); } isCBX(book: Book): boolean { @@ -1168,7 +1170,7 @@ export class MetadataEditorComponent implements OnInit { } getUploadAudiobookCoverUrl(): string { - return this.bookService.getUploadAudiobookCoverUrl(this.currentBookId); + return this.bookMetadataManageService.getUploadAudiobookCoverUrl(this.currentBookId); } openAudiobookCoverSearch() { diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts index 8efa3570d..f71b77e76 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts @@ -9,6 +9,7 @@ import {forkJoin, Observable} from 'rxjs'; import {Tooltip} from 'primeng/tooltip'; import {UrlHelperService} from '../../../../../shared/service/url-helper.service'; import {BookService} from '../../../../book/service/book.service'; +import {BookMetadataManageService} from '../../../../book/service/book-metadata-manage.service'; import {Textarea} from 'primeng/textarea'; import {filter, take} from 'rxjs/operators'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @@ -78,6 +79,7 @@ export class MetadataPickerComponent implements OnInit { private messageService = inject(MessageService); private bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); protected urlHelper = inject(UrlHelperService); private destroyRef = inject(DestroyRef); private appSettingsService = inject(AppSettingsService); @@ -287,14 +289,14 @@ export class MetadataPickerComponent implements OnInit { const updatedBookMetadata = this.buildMetadataWrapper(undefined); const requests: Observable[] = [ - this.bookService.updateBookMetadata(this.currentBookId, updatedBookMetadata, false) + this.bookMetadataManageService.updateBookMetadata(this.currentBookId, updatedBookMetadata, false) ]; // Handle audiobook cover upload when fetched from Audible provider if (this.isAudibleProvider() && this.copiedFields['audiobookThumbnailUrl']) { const audiobookCoverUrl = this.fetchedMetadata.thumbnailUrl; if (audiobookCoverUrl) { - requests.push(this.bookService.uploadAudiobookCoverFromUrl(this.currentBookId, audiobookCoverUrl)); + requests.push(this.bookMetadataManageService.uploadAudiobookCoverFromUrl(this.currentBookId, audiobookCoverUrl)); } } @@ -515,7 +517,7 @@ export class MetadataPickerComponent implements OnInit { } private updateMetadata(shouldLockAllFields: boolean | undefined): void { - this.bookService.updateBookMetadata(this.currentBookId, this.buildMetadataWrapper(shouldLockAllFields), false).subscribe({ + this.bookMetadataManageService.updateBookMetadata(this.currentBookId, this.buildMetadataWrapper(shouldLockAllFields), false).subscribe({ next: () => { if (shouldLockAllFields !== undefined) { this.messageService.add({ diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-tabs/metadata-tabs.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-tabs/metadata-tabs.component.ts index 53a251987..0c0f6d198 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-tabs/metadata-tabs.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-tabs/metadata-tabs.component.ts @@ -11,7 +11,7 @@ import {Button} from 'primeng/button'; import {Tooltip} from 'primeng/tooltip'; import {Image} from 'primeng/image'; import {UrlHelperService} from '../../../../../../shared/service/url-helper.service'; -import {BookService} from '../../../../../book/service/book.service'; +import {BookMetadataManageService} from '../../../../../book/service/book-metadata-manage.service'; import {TranslocoDirective} from '@jsverse/transloco'; export interface ReadEvent { @@ -76,7 +76,7 @@ export class MetadataTabsComponent { @Input() recommendedBooks: BookRecommendation[] = []; protected urlHelper = inject(UrlHelperService); - private bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); @Output() readBook = new EventEmitter(); @Output() downloadBook = new EventEmitter(); @@ -172,6 +172,6 @@ export class MetadataTabsComponent { } supportsDualCovers(): boolean { - return this.bookService.supportsDualCovers(this.book); + return this.bookMetadataManageService.supportsDualCovers(this.book); } } diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts index 1d2c83418..308b0179a 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts @@ -3,6 +3,7 @@ import {Button} from 'primeng/button'; import {AsyncPipe, DecimalPipe, NgClass} from '@angular/common'; import {combineLatest, Observable} from 'rxjs'; import {BookService} from '../../../../book/service/book.service'; +import {BookFileService} from '../../../../book/service/book-file.service'; import {Rating, RatingRateEvent} from 'primeng/rating'; import {FormsModule} from '@angular/forms'; import {Book, BookFile, BookMetadata, BookRecommendation, BookType, ComicMetadata, FileInfo, ReadStatus} from '../../../../book/model/book.model'; @@ -54,6 +55,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges { private emailService = inject(EmailService); private messageService = inject(MessageService); private bookService = inject(BookService); + private bookFileService = inject(BookFileService); private taskHelperService = inject(TaskHelperService); protected urlHelper = inject(UrlHelperService); protected userService = inject(UserService); @@ -484,11 +486,11 @@ export class MetadataViewerComponent implements OnInit, OnChanges { } download(book: Book) { - this.bookService.downloadFile(book); + this.bookFileService.downloadFile(book); } downloadAdditionalFile(book: Book, fileId: number) { - this.bookService.downloadAdditionalFile(book, fileId); + this.bookFileService.downloadAdditionalFile(book, fileId); } // Event handlers for MetadataTabsComponent @@ -505,7 +507,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges { } onDownloadAllFiles(event: DownloadAllFilesEvent): void { - this.bookService.downloadAllFiles(event.book); + this.bookFileService.downloadAllFiles(event.book); } onDeleteBookFile(event: DeleteBookFileEvent): void { @@ -526,7 +528,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges { rejectButtonStyleClass: 'p-button-secondary', acceptButtonStyleClass: 'p-button-danger', accept: () => { - this.bookService.deleteAdditionalFile(bookId, fileId).subscribe({ + this.bookFileService.deleteAdditionalFile(bookId, fileId).subscribe({ next: () => { this.messageService.add({ severity: 'success', @@ -572,7 +574,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges { rejectButtonStyleClass: 'p-button-secondary', acceptButtonStyleClass: 'p-button-danger', accept: () => { - this.bookService.deleteBookFile(book.id, fileId, isPrimary).subscribe({ + this.bookFileService.deleteBookFile(book.id, fileId, isPrimary).subscribe({ next: () => { this.messageService.add({ severity: 'success', diff --git a/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.ts b/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.ts index b8fe551ff..169d25d6d 100644 --- a/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.ts +++ b/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.ts @@ -8,6 +8,7 @@ import {DatePicker} from 'primeng/datepicker'; import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; import {MessageService} from 'primeng/api'; import {BookService} from '../../../book/service/book.service'; +import {BookMetadataManageService} from '../../../book/service/book-metadata-manage.service'; import {Book, BulkMetadataUpdateRequest} from '../../../book/model/book.model'; import {Checkbox} from 'primeng/checkbox'; import {AutoComplete} from 'primeng/autocomplete'; @@ -60,6 +61,7 @@ export class BulkMetadataUpdateComponent implements OnInit { readonly ref = inject(DynamicDialogRef); private readonly fb = inject(FormBuilder); private readonly bookService = inject(BookService); + private readonly bookMetadataManageService = inject(BookMetadataManageService); private readonly messageService = inject(MessageService); allAuthors!: string[]; @@ -257,10 +259,10 @@ export class BulkMetadataUpdateComponent implements OnInit { }; this.loading = true; - this.bookService.updateBooksMetadata(payload).subscribe({ + this.bookMetadataManageService.updateBooksMetadata(payload).subscribe({ next: () => { if (this.selectedCoverFile) { - this.bookService.bulkUploadCover(this.bookIds, this.selectedCoverFile).subscribe({ + this.bookMetadataManageService.bulkUploadCover(this.bookIds, this.selectedCoverFile).subscribe({ next: () => { this.loading = false; this.messageService.add({ diff --git a/booklore-ui/src/app/features/metadata/component/cover-search/cover-search.component.ts b/booklore-ui/src/app/features/metadata/component/cover-search/cover-search.component.ts index d054552ab..9d7562d90 100644 --- a/booklore-ui/src/app/features/metadata/component/cover-search/cover-search.component.ts +++ b/booklore-ui/src/app/features/metadata/component/cover-search/cover-search.component.ts @@ -8,6 +8,7 @@ import {InputText} from 'primeng/inputtext'; import {ProgressSpinner} from 'primeng/progressspinner'; import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; import {BookService} from '../../../book/service/book.service'; +import {BookMetadataManageService} from '../../../book/service/book-metadata-manage.service'; import {Image} from 'primeng/image'; import {Tooltip} from 'primeng/tooltip'; import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; @@ -40,6 +41,7 @@ export class CoverSearchComponent implements OnInit { private dynamicDialogConfig = inject(DynamicDialogConfig); protected dynamicDialogRef = inject(DynamicDialogRef); protected bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); private messageService = inject(MessageService); private readonly t = inject(TranslocoService); @@ -107,8 +109,8 @@ export class CoverSearchComponent implements OnInit { selectAndSave(image: CoverImage) { const uploadObservable = this.coverType === 'audiobook' - ? this.bookService.uploadAudiobookCoverFromUrl(this.bookId, image.url) - : this.bookService.uploadCoverFromUrl(this.bookId, image.url); + ? this.bookMetadataManageService.uploadAudiobookCoverFromUrl(this.bookId, image.url) + : this.bookMetadataManageService.uploadCoverFromUrl(this.bookId, image.url); uploadObservable.subscribe({ next: () => { diff --git a/booklore-ui/src/app/features/metadata/component/metadata-manager/metadata-manager.component.ts b/booklore-ui/src/app/features/metadata/component/metadata-manager/metadata-manager.component.ts index 8d4feadd1..b4ea22989 100644 --- a/booklore-ui/src/app/features/metadata/component/metadata-manager/metadata-manager.component.ts +++ b/booklore-ui/src/app/features/metadata/component/metadata-manager/metadata-manager.component.ts @@ -10,6 +10,7 @@ import {ConfirmDialogModule} from 'primeng/confirmdialog'; import {ConfirmationService, MessageService} from 'primeng/api'; import {PageTitleService} from "../../../../shared/service/page-title.service"; import {BookService} from '../../../book/service/book.service'; +import {BookMetadataManageService} from '../../../book/service/book-metadata-manage.service'; import {Book} from '../../../book/model/book.model'; import {FormsModule} from '@angular/forms'; import {Tooltip} from 'primeng/tooltip'; @@ -67,6 +68,7 @@ interface TabConfig { }) export class MetadataManagerComponent implements OnInit, OnDestroy { private bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); private messageService = inject(MessageService); private router = inject(Router); private route = inject(ActivatedRoute); @@ -326,7 +328,7 @@ export class MetadataManagerComponent implements OnInit, OnDestroy { this.loading = true; this.mergingInProgress = true; - this.bookService.consolidateMetadata(this.currentMergeType, targetValues, [oldValue]).subscribe({ + this.bookMetadataManageService.consolidateMetadata(this.currentMergeType, targetValues, [oldValue]).subscribe({ next: () => { this.messageService.add({ severity: 'success', @@ -403,7 +405,7 @@ export class MetadataManagerComponent implements OnInit, OnDestroy { this.loading = true; this.mergingInProgress = true; - this.bookService.consolidateMetadata(this.currentMergeType, targetValues, valuesToMerge).subscribe({ + this.bookMetadataManageService.consolidateMetadata(this.currentMergeType, targetValues, valuesToMerge).subscribe({ next: () => { this.messageService.add({ severity: 'success', @@ -463,7 +465,7 @@ export class MetadataManagerComponent implements OnInit, OnDestroy { this.loading = true; this.deletingInProgress = true; - this.bookService.deleteMetadata(this.currentMergeType, valuesToDelete).subscribe({ + this.bookMetadataManageService.deleteMetadata(this.currentMergeType, valuesToDelete).subscribe({ next: () => { this.messageService.add({ severity: 'success', diff --git a/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.ts b/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.ts index 015511557..a17df221b 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.ts @@ -16,6 +16,7 @@ import {ReaderLeftSidebarService} from './layout/panel/panel.service'; import {ReaderHeaderService} from './layout/header/header.service'; import {ReaderNoteService} from './features/notes/note.service'; import {BookService} from '../../book/service/book.service'; +import {BookFileService} from '../../book/service/book-file.service'; import {ActivatedRoute} from '@angular/router'; import {Book, BookType} from '../../book/model/book.model'; import {ReaderHeaderComponent} from './layout/header/header.component'; @@ -73,6 +74,7 @@ export class EbookReaderComponent implements OnInit, OnDestroy { private loaderService = inject(ReaderLoaderService); private styleService = inject(ReaderStyleService); private bookService = inject(BookService); + private bookFileService = inject(BookFileService); private route = inject(ActivatedRoute); private epubCustomFontService = inject(EpubCustomFontService); private annotationService = inject(ReaderAnnotationHttpService); @@ -283,7 +285,7 @@ export class EbookReaderComponent implements OnInit, OnDestroy { } private loadBookBlob(): Observable { - return this.bookService.getFileContent(this.bookId, this.altBookType).pipe( + return this.bookFileService.getFileContent(this.bookId, this.altBookType).pipe( switchMap(fileBlob => { const fileUrl = URL.createObjectURL(fileBlob); this._fileUrl = fileUrl; diff --git a/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.ts b/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.ts index 1be0b302a..bdc865a0c 100644 --- a/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.ts +++ b/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.ts @@ -6,7 +6,7 @@ import {ToggleSwitch} from 'primeng/toggleswitch'; import {MessageService} from 'primeng/api'; import {AppSettingsService} from '../../../shared/service/app-settings.service'; -import {BookService} from '../../book/service/book.service'; +import {BookMetadataManageService} from '../../book/service/book-metadata-manage.service'; import {AppSettingKey, AppSettings, CoverCroppingSettings} from '../../../shared/model/app-settings.model'; import {filter, take} from 'rxjs/operators'; import {InputText} from 'primeng/inputtext'; @@ -46,7 +46,7 @@ export class GlobalPreferencesComponent implements OnInit { }; private appSettingsService = inject(AppSettingsService); - private bookService = inject(BookService); + private bookMetadataManageService = inject(BookMetadataManageService); private messageService = inject(MessageService); private t = inject(TranslocoService); @@ -98,7 +98,7 @@ export class GlobalPreferencesComponent implements OnInit { } regenerateCovers(): void { - this.bookService.regenerateCovers().subscribe({ + this.bookMetadataManageService.regenerateCovers().subscribe({ next: () => this.showMessage('success', this.t.translate('settingsApp.covers.regenerateStarted'), this.t.translate('settingsApp.covers.regenerateStartedDetail')), error: () =>