fix(metadata): improve cover errors, reduce metadata fetch count, and hide attach option (#2690)

* fix(metadata): improve cover regen errors and reduce detailed metadata fetch count

* fix(metadata): hide attach-to-book option in single-format libraries

---------

Co-authored-by: acx10 <acx10@users.noreply.github.com>
This commit is contained in:
ACX
2026-02-10 21:14:46 -07:00
committed by GitHub
parent ff11ec57b8
commit 6da6e8e373
5 changed files with 13 additions and 8 deletions

View File

@@ -34,7 +34,7 @@ public enum ApiError {
SCHEDULE_REFRESH_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to schedule metadata refresh job. Error: %s"),
ANOTHER_METADATA_JOB_RUNNING(HttpStatus.CONFLICT, "A metadata refresh job is currently running. Please wait for it to complete before initiating a new one."),
METADATA_SOURCE_NOT_IMPLEMENT_OR_DOES_NOT_EXIST(HttpStatus.BAD_REQUEST, "Metadata source not implement or does not exist"),
FAILED_TO_REGENERATE_COVER(HttpStatus.BAD_REQUEST, "Failed to regenerate cover"),
FAILED_TO_REGENERATE_COVER(HttpStatus.BAD_REQUEST, "Failed to regenerate cover: %s"),
NO_COVER_IN_FILE(HttpStatus.BAD_REQUEST, "No embedded cover image found in the audiobook file"),
FAILED_TO_DOWNLOAD_FILE(HttpStatus.INTERNAL_SERVER_ERROR, "Error while downloading file, bookId: %s"),
INVALID_REFRESH_TYPE(HttpStatus.BAD_REQUEST, "The refresh type is invalid"),

View File

@@ -172,12 +172,12 @@ public class BookCoverService {
var audiobookFile = bookEntity.getBookFiles().stream()
.filter(f -> f.getBookType() == BookFileType.AUDIOBOOK)
.findFirst()
.orElseThrow(ApiError.FAILED_TO_REGENERATE_COVER::createException);
.orElseThrow(() -> ApiError.FAILED_TO_REGENERATE_COVER.createException("no audiobook file found"));
BookFileProcessor processor = processorRegistry.getProcessorOrThrow(audiobookFile.getBookType());
boolean success = processor.generateAudiobookCover(bookEntity);
if (!success) {
throw ApiError.FAILED_TO_REGENERATE_COVER.createException();
throw ApiError.FAILED_TO_REGENERATE_COVER.createException("no embedded cover image found in the audiobook file");
}
updateAudiobookCoverMetadata(bookEntity);
bookRepository.save(bookEntity);
@@ -233,13 +233,13 @@ public class BookCoverService {
BookFileEntity ebookFile = findEbookFile(bookEntity);
if (ebookFile == null) {
throw ApiError.FAILED_TO_REGENERATE_COVER.createException();
throw ApiError.FAILED_TO_REGENERATE_COVER.createException("no ebook file found for the book");
}
BookFileProcessor processor = processorRegistry.getProcessorOrThrow(ebookFile.getBookType());
boolean success = processor.generateCover(bookEntity, ebookFile);
if (!success) {
throw ApiError.FAILED_TO_REGENERATE_COVER.createException();
throw ApiError.FAILED_TO_REGENERATE_COVER.createException("no embedded cover image found in the file");
}
updateBookCoverMetadata(bookEntity);
bookRepository.save(bookEntity);

View File

@@ -45,7 +45,7 @@ public class AmazonBookParser implements BookParser, DetailedMetadataProvider {
}
}
private static final int COUNT_DETAILED_METADATA_TO_GET = 4;
private static final int COUNT_DETAILED_METADATA_TO_GET = 3;
private static final String BASE_BOOK_URL_SUFFIX = "/dp/";
private static final Pattern NON_DIGIT_PATTERN = Pattern.compile("[^\\d]");
private static final Pattern SERIES_FORMAT_PATTERN = Pattern.compile("Book (\\d+(?:\\.\\d+)?) of (\\d+)");

View File

@@ -36,7 +36,7 @@ import java.util.stream.Collectors;
@AllArgsConstructor
public class AudibleParser implements BookParser, DetailedMetadataProvider {
private static final int COUNT_DETAILED_METADATA_TO_GET = 4;
private static final int COUNT_DETAILED_METADATA_TO_GET = 3;
private static final long MIN_REQUEST_INTERVAL_MS = 1500;
private static final String DEFAULT_DOMAIN = "com";

View File

@@ -26,6 +26,7 @@ import {ProgressSpinner} from 'primeng/progressspinner';
import {TieredMenu} from 'primeng/tieredmenu';
import {Image} from 'primeng/image';
import {BookDialogHelperService} from '../../../../book/components/book-browser/book-dialog-helper.service';
import {LibraryService} from '../../../../book/service/library.service';
import {TagColor, TagComponent} from '../../../../../shared/components/tag/tag.component';
import {TaskHelperService} from '../../../../settings/task-management/task-helper.service';
import {AGE_RATING_OPTIONS, CONTENT_RATING_LABELS, fileSizeRanges, matchScoreRanges, pageCountRanges} from '../../../../book/components/book-browser/book-filter/book-filter.config';
@@ -50,6 +51,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges {
private originalRecommendedBooks: BookRecommendation[] = [];
private readonly t = inject(TranslocoService);
private libraryService = inject(LibraryService);
private bookDialogHelperService = inject(BookDialogHelperService)
private emailService = inject(EmailService);
private messageService = inject(MessageService);
@@ -267,8 +269,11 @@ export class MetadataViewerComponent implements OnInit, OnChanges {
}
// Show "Attach to Another Book" for single-file books (detached books) - not for physical books
// Hide in single-format libraries where attaching provides no cross-format benefit
const isSingleFileBook = hasFiles && !book.alternativeFormats?.length;
if (isSingleFileBook && (userState?.user?.permissions.canManageLibrary || userState?.user?.permissions.admin)) {
const library = this.libraryService.findLibraryById(book.libraryId);
const isMultiFormatLibrary = !library?.allowedFormats?.length || library.allowedFormats.length > 1;
if (isSingleFileBook && isMultiFormatLibrary && (userState?.user?.permissions.canManageLibrary || userState?.user?.permissions.admin)) {
items.push({
label: this.t.translate('metadata.viewer.menuAttachToAnotherBook'),
icon: 'pi pi-link',