refactor: split BookService into BookFileService and BookMetadataManageService (#2758)

This commit is contained in:
ACX
2026-02-14 22:07:57 -07:00
committed by GitHub
parent 0899b99188
commit c7f0a910e0
23 changed files with 543 additions and 475 deletions

View File

@@ -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,

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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);

View File

@@ -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

View File

@@ -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: () => {

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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<Blob> {
let url = `${this.url}/${bookId}/content`;
if (bookType) {
url += `?bookType=${bookType}`;
}
return this.http.get<Blob>(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<void> {
const deleteUrl = `${this.url}/${bookId}/files/${fileId}`;
return this.http.delete<void>(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<void> {
const deleteUrl = `${this.url}/${bookId}/files/${fileId}`;
return this.http.delete<void>(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<AdditionalFile> {
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<AdditionalFile>(`${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<Book> {
return this.http.post<Book>(`${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);
})
);
}
}

View File

@@ -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)))

View File

@@ -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<BookMetadata> {
const params = new HttpParams().set('mergeCategories', mergeCategories.toString());
return this.http.put<BookMetadata>(`${this.url}/${bookId}/metadata`, wrapper, {params}).pipe(
map(updatedMetadata => {
this.bookSocketService.handleBookMetadataUpdate(bookId!, updatedMetadata);
return updatedMetadata;
})
);
}
updateBooksMetadata(request: BulkMetadataUpdateRequest): Observable<void> {
return this.http.put(`${this.url}/bulk-edit-metadata`, request).pipe(
map(() => void 0)
);
}
toggleAllLock(bookIds: Set<number>, lock: string): Observable<void> {
const requestBody = {
bookIds: Array.from(bookIds),
lock: lock
};
return this.http.put<BookMetadata[]>(`${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<number>, fieldActions: Record<string, 'LOCK' | 'UNLOCK'>): Observable<void> {
const bookIdSet = bookIds instanceof Set ? bookIds : new Set(bookIds);
const requestBody = {
bookIds: Array.from(bookIdSet),
fieldActions
};
return this.http.put<void>(`${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<string, unknown>)[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<unknown> {
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<unknown> {
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<BookMetadata> {
return this.http.post<BookMetadata>(`${this.url}/${bookId}/metadata/cover/from-url`, {url});
}
regenerateCovers(): Observable<void> {
return this.http.post<void>(`${this.url}/regenerate-covers`, {});
}
regenerateCover(bookId: number): Observable<void> {
return this.http.post<void>(`${this.url}/${bookId}/regenerate-cover`, {});
}
getFileMetadata(bookId: number): Observable<BookMetadata> {
return this.http.get<BookMetadata>(`${this.url}/${bookId}/file-metadata`);
}
generateCustomCover(bookId: number): Observable<void> {
return this.http.post<void>(`${this.url}/${bookId}/generate-custom-cover`, {});
}
generateCustomCoversForBooks(bookIds: number[]): Observable<void> {
return this.http.post<void>(`${this.url}/bulk-generate-custom-covers`, {bookIds});
}
regenerateCoversForBooks(bookIds: number[]): Observable<void> {
return this.http.post<void>(`${this.url}/bulk-regenerate-covers`, {bookIds});
}
uploadAudiobookCoverFromUrl(bookId: number, url: string): Observable<BookMetadata> {
return this.http.post<BookMetadata>(`${this.url}/${bookId}/metadata/audiobook-cover/from-url`, {url});
}
uploadAudiobookCoverFromFile(bookId: number, file: File): Observable<void> {
const formData = new FormData();
formData.append('file', file);
return this.http.post<void>(`${this.url}/${bookId}/metadata/audiobook-cover/upload`, formData);
}
getUploadAudiobookCoverUrl(bookId: number): string {
return this.url + '/' + bookId + "/metadata/audiobook-cover/upload";
}
regenerateAudiobookCover(bookId: number): Observable<void> {
return this.http.post<void>(`${this.url}/${bookId}/regenerate-audiobook-cover`, {});
}
generateCustomAudiobookCover(bookId: number): Observable<void> {
return this.http.post<void>(`${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<void> {
const formData = new FormData();
formData.append('file', file);
formData.append('bookIds', bookIds.join(','));
return this.http.post<void>(`${this.url}/bulk-upload-cover`, formData);
}
}

View File

@@ -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',

View File

@@ -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<void>(`${this.url}/${bookId}/viewer-setting`, bookSetting);
}
/*------------------ File Operations ------------------*/
getFileContent(bookId: number, bookType?: string): Observable<Blob> {
let url = `${this.url}/${bookId}/content`;
if (bookType) {
url += `?bookType=${bookType}`;
}
return this.http.get<Blob>(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<void> {
const deleteUrl = `${this.url}/${bookId}/files/${fileId}`;
return this.http.delete<void>(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<void> {
const deleteUrl = `${this.url}/${bookId}/files/${fileId}`;
return this.http.delete<void>(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<AdditionalFile> {
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<AdditionalFile>(`${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<BookMetadata> {
const params = new HttpParams().set('mergeCategories', mergeCategories.toString());
return this.http.put<BookMetadata>(`${this.url}/${bookId}/metadata`, wrapper, {params}).pipe(
map(updatedMetadata => {
this.handleBookMetadataUpdate(bookId!, updatedMetadata);
return updatedMetadata;
})
);
}
updateBooksMetadata(request: BulkMetadataUpdateRequest): Observable<void> {
return this.http.put(`${this.url}/bulk-edit-metadata`, request).pipe(
map(() => void 0)
);
}
toggleAllLock(bookIds: Set<number>, lock: string): Observable<void> {
const requestBody = {
bookIds: Array.from(bookIds),
lock: lock
};
return this.http.put<BookMetadata[]>(`${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<number>, fieldActions: Record<string, 'LOCK' | 'UNLOCK'>): Observable<void> {
const bookIdSet = bookIds instanceof Set ? bookIds : new Set(bookIds);
const requestBody = {
bookIds: Array.from(bookIdSet),
fieldActions
};
return this.http.put<void>(`${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<string, unknown>)[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<unknown> {
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<unknown> {
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<BookMetadata> {
return this.http.post<BookMetadata>(`${this.url}/${bookId}/metadata/cover/from-url`, {url});
}
regenerateCovers(): Observable<void> {
return this.http.post<void>(`${this.url}/regenerate-covers`, {});
}
regenerateCover(bookId: number): Observable<void> {
return this.http.post<void>(`${this.url}/${bookId}/regenerate-cover`, {});
}
getFileMetadata(bookId: number): Observable<BookMetadata> {
return this.http.get<BookMetadata>(`${this.url}/${bookId}/file-metadata`);
}
generateCustomCover(bookId: number): Observable<void> {
return this.http.post<void>(`${this.url}/${bookId}/generate-custom-cover`, {});
}
generateCustomCoversForBooks(bookIds: number[]): Observable<void> {
return this.http.post<void>(`${this.url}/bulk-generate-custom-covers`, {bookIds});
}
regenerateCoversForBooks(bookIds: number[]): Observable<void> {
return this.http.post<void>(`${this.url}/bulk-regenerate-covers`, {bookIds});
}
uploadAudiobookCoverFromUrl(bookId: number, url: string): Observable<BookMetadata> {
return this.http.post<BookMetadata>(`${this.url}/${bookId}/metadata/audiobook-cover/from-url`, {url});
}
uploadAudiobookCoverFromFile(bookId: number, file: File): Observable<void> {
const formData = new FormData();
formData.append('file', file);
return this.http.post<void>(`${this.url}/${bookId}/metadata/audiobook-cover/upload`, formData);
}
getUploadAudiobookCoverUrl(bookId: number): string {
return this.url + '/' + bookId + "/metadata/audiobook-cover/upload";
}
regenerateAudiobookCover(bookId: number): Observable<void> {
return this.http.post<void>(`${this.url}/${bookId}/regenerate-audiobook-cover`, {});
}
generateCustomAudiobookCover(bookId: number): Observable<void> {
return this.http.post<void>(`${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<void> {
const formData = new FormData();
formData.append('file', file);
formData.append('bookIds', bookIds.join(','));
return this.http.post<void>(`${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<Book> {
return this.http.post<Book>(`${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);
})
);
}
}

View File

@@ -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<void> {
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() {

View File

@@ -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<unknown>[] = [
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({

View File

@@ -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<ReadEvent>();
@Output() downloadBook = new EventEmitter<DownloadEvent>();
@@ -172,6 +172,6 @@ export class MetadataTabsComponent {
}
supportsDualCovers(): boolean {
return this.bookService.supportsDualCovers(this.book);
return this.bookMetadataManageService.supportsDualCovers(this.book);
}
}

View File

@@ -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',

View File

@@ -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({

View File

@@ -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: () => {

View File

@@ -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',

View File

@@ -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<void> {
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;

View File

@@ -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: () =>