fix(books): respect library cover aspect ratio for combined media books

Books containing both ebook and audiobook formats were incorrectly rendered with square covers in dashboard widgets, series views, and cards because aspect ratio logic checked for any audio format file instead of relying on the configured cover aspect ratio or primary media profile.

Closes #712
This commit is contained in:
neon
2026-07-22 04:12:13 +00:00
parent d051039a2d
commit 544f4d5e53
46 changed files with 499 additions and 133 deletions
+37
View File
@@ -4,6 +4,7 @@ import type { AudiobookChapter, NarratorRef } from "./audiobook";
import type { ComicMetadataFields } from "./metadata-fetch";
import type { BookFileWriteField, WriteResult } from "./file-write";
import type { CustomMetadataBookValue } from "./custom-metadata";
import type { CoverAspectRatio } from "./library";
export const BOOK_FORMATS = ["epub", "pdf", "mobi", "azw3", "cbz", "cbr", "cb7", "fb2", "m4b", "mp3", "m4a", "opus", "ogg", "flac"] as const;
export type BookFormat = (typeof BOOK_FORMATS)[number];
@@ -75,6 +76,39 @@ export type BookFileRef = {
sizeBytes: number | null;
};
export type BookMediaKind = "ebook" | "audiobook" | "comic" | "unknown";
export type BookMediaProfile = {
primaryMediaKind: BookMediaKind;
hasEbook: boolean;
hasAudio: boolean;
hasComic: boolean;
};
type BookMediaFile = Pick<BookFileRef, "format" | "role">;
export function getPrimaryBookFile<T extends BookMediaFile>(files: readonly T[]): T | null {
return files.find((file) => file.role === "primary") ?? files.find((file) => file.format != null) ?? files[0] ?? null;
}
export function getBookMediaKind(format: string | null | undefined): BookMediaKind {
const normalized = format?.trim().toLowerCase();
if (!normalized) return "unknown";
if (isAudioFormat(normalized)) return "audiobook";
if (isComicFormat(normalized)) return "comic";
return "ebook";
}
export function getBookMediaProfile(files: readonly BookMediaFile[]): BookMediaProfile {
const mediaKinds = files.map((file) => getBookMediaKind(file.format));
return {
primaryMediaKind: getBookMediaKind(getPrimaryBookFile(files)?.format),
hasEbook: mediaKinds.includes("ebook"),
hasAudio: mediaKinds.includes("audiobook"),
hasComic: mediaKinds.includes("comic"),
};
}
export type BookSeriesMembership = {
seriesId: number;
seriesName: string;
@@ -85,6 +119,7 @@ export type BookSeriesMembership = {
export type BookCard = {
id: number;
status: string;
coverAspectRatio: CoverAspectRatio;
title: string | null;
authors: string[];
seriesId?: number | null;
@@ -282,6 +317,7 @@ export type BooksPage = {
export type BookRecommendation = {
id: number;
title: string | null;
coverAspectRatio: CoverAspectRatio;
updatedAt: string | null;
hasCover: boolean;
authors: string[];
@@ -292,6 +328,7 @@ export type BookRecommendation = {
export type SeriesBookRecommendation = {
id: number;
title: string | null;
coverAspectRatio: CoverAspectRatio;
updatedAt: string | null;
seriesIndex: number | null;
hasCover: boolean;
+4
View File
@@ -1,6 +1,10 @@
export type OrganizationMode = "book_per_file" | "book_per_folder";
export type CoverAspectRatio = "2/3" | "1/1";
export function normalizeCoverAspectRatio(value: unknown): CoverAspectRatio {
return value === "1/1" ? "1/1" : "2/3";
}
export const DEFAULT_FORMAT_PRIORITY = [
"epub",
"kepub",