mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(metadata): persist Hardcover edition ID during automatic refresh
During an automatic metadata refresh, the fetched Hardcover edition ID was not being copied to the update payload. This ensures the value is persisted to the database alongside the other metadata fields. Closes #440
This commit is contained in:
@@ -5,7 +5,7 @@ export const PROVIDER_ID_FIELDS = [
|
||||
{ provider: MetadataProviderKey.GOODREADS, field: 'goodreadsId', label: 'Goodreads' },
|
||||
{ provider: MetadataProviderKey.AMAZON, field: 'amazonId', label: 'Amazon' },
|
||||
{ provider: MetadataProviderKey.HARDCOVER, field: 'hardcoverId', label: 'Hardcover' },
|
||||
{ provider: MetadataProviderKey.HARDCOVER, field: 'hardcoverEditionId', label: 'Hardcover Edition' },
|
||||
{ provider: MetadataProviderKey.HARDCOVER, field: 'hardcoverEditionId', label: 'Hardcover Ed.' },
|
||||
{ provider: MetadataProviderKey.OPEN_LIBRARY, field: 'openLibraryId', label: 'OpenLibrary' },
|
||||
{ provider: MetadataProviderKey.ITUNES, field: 'itunesId', label: 'iTunes' },
|
||||
{ provider: MetadataProviderKey.AUDIBLE, field: 'audibleId', label: 'Audible' },
|
||||
|
||||
@@ -46,88 +46,3 @@ export const BOOK_METADATA_LOCK_FIELDS = [
|
||||
] as const;
|
||||
|
||||
export type BookMetadataLockField = (typeof BOOK_METADATA_LOCK_FIELDS)[number];
|
||||
|
||||
export type BookMetadataLockGroupKey = "core" | "series" | "classification" | "audio" | "providers" | "comic" | "assets";
|
||||
|
||||
export interface BookMetadataLockGroup {
|
||||
key: BookMetadataLockGroupKey;
|
||||
label: string;
|
||||
fields: BookMetadataLockField[];
|
||||
}
|
||||
|
||||
export const BOOK_METADATA_LOCK_GROUPS: BookMetadataLockGroup[] = [
|
||||
{
|
||||
key: "core",
|
||||
label: "Core metadata",
|
||||
fields: [
|
||||
"title",
|
||||
"subtitle",
|
||||
"authors",
|
||||
"description",
|
||||
"publisher",
|
||||
"publishedYear",
|
||||
"language",
|
||||
"pageCount",
|
||||
"isbn13",
|
||||
"isbn10",
|
||||
"rating",
|
||||
"communityRating",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "series",
|
||||
label: "Series",
|
||||
fields: ["seriesName", "seriesIndex"],
|
||||
},
|
||||
{
|
||||
key: "classification",
|
||||
label: "Classification",
|
||||
fields: ["genres", "tags"],
|
||||
},
|
||||
{
|
||||
key: "audio",
|
||||
label: "Audio",
|
||||
fields: ["narrators", "durationSeconds", "abridged"],
|
||||
},
|
||||
{
|
||||
key: "providers",
|
||||
label: "Provider IDs",
|
||||
fields: [
|
||||
"googleBooksId",
|
||||
"goodreadsId",
|
||||
"amazonId",
|
||||
"hardcoverId",
|
||||
"hardcoverEditionId",
|
||||
"openLibraryId",
|
||||
"itunesId",
|
||||
"audibleId",
|
||||
"koboId",
|
||||
"comicvineId",
|
||||
"ranobedbId",
|
||||
"lubimyczytacId",
|
||||
"aladinId",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "comic",
|
||||
label: "Comic",
|
||||
fields: [
|
||||
"comicIssueNumber",
|
||||
"comicVolumeName",
|
||||
"comicStoryArcs",
|
||||
"comicPencillers",
|
||||
"comicInkers",
|
||||
"comicColorists",
|
||||
"comicLetterers",
|
||||
"comicCoverArtists",
|
||||
"comicCharacters",
|
||||
"comicTeams",
|
||||
"comicLocations",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "assets",
|
||||
label: "Assets",
|
||||
fields: ["cover"],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1127,6 +1127,45 @@ describe('BookService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('refreshMetadata persists Hardcover edition id returned by pipeline', async () => {
|
||||
const { service, bookRepo, pipeline } = makeService();
|
||||
const user = makeUser();
|
||||
bookRepo.findById.mockResolvedValue({
|
||||
book: {
|
||||
books: { id: 1, libraryId: 7 },
|
||||
book_metadata: { title: 'Old', isbn13: '9780756404741', isbn10: null, hardcoverEditionId: null },
|
||||
},
|
||||
authorRows: [{ id: 1, name: 'Patrick Rothfuss', sortName: null }],
|
||||
genreRows: [],
|
||||
communityRatingRows: [],
|
||||
});
|
||||
pipeline.runWithSources.mockResolvedValue({
|
||||
resolved: { hardcoverEditionId: '8941973' },
|
||||
sources: {},
|
||||
providerIds: {
|
||||
[MetadataProviderKey.HARDCOVER]: 'the-name-of-the-wind',
|
||||
},
|
||||
});
|
||||
|
||||
const updateSpy = vi.spyOn(service, 'updateMetadata').mockResolvedValue({
|
||||
book: { id: 1 },
|
||||
write: null,
|
||||
libraryAutoWriteEnabled: false,
|
||||
} as never);
|
||||
|
||||
await service.refreshMetadata(1, false, user);
|
||||
|
||||
expect(updateSpy).toHaveBeenCalledWith(
|
||||
1,
|
||||
{
|
||||
hardcoverEditionId: '8941973',
|
||||
hardcoverId: 'the-name-of-the-wind',
|
||||
},
|
||||
user,
|
||||
{ postSaveMode: 'schedule' },
|
||||
);
|
||||
});
|
||||
|
||||
it('refreshMetadata persists provider-specific community rating rows', async () => {
|
||||
const { service, bookRepo, pipeline } = makeService();
|
||||
const user = makeUser();
|
||||
@@ -1318,7 +1357,7 @@ describe('BookService', () => {
|
||||
genreRows: [],
|
||||
});
|
||||
pipeline.runWithSources.mockResolvedValue({
|
||||
resolved: { title: 'Resolved', authors: ['A'], coverUrl: 'https://img/c.jpg' },
|
||||
resolved: { title: 'Resolved', authors: ['A'], coverUrl: 'https://img/c.jpg', hardcoverEditionId: '8941973' },
|
||||
sources: {},
|
||||
providerIds: {
|
||||
[MetadataProviderKey.GOOGLE]: 'g-id',
|
||||
@@ -1327,7 +1366,7 @@ describe('BookService', () => {
|
||||
bookMetadataLockService.filterResolvedMetadata.mockResolvedValue({
|
||||
resolved: { authors: ['A'] },
|
||||
providerIds: {},
|
||||
skippedFields: ['title', 'cover', 'googleBooksId'],
|
||||
skippedFields: ['title', 'cover', 'googleBooksId', 'hardcoverEditionId'],
|
||||
});
|
||||
|
||||
const updateSpy = vi.spyOn(service, 'updateMetadata').mockResolvedValue({
|
||||
@@ -1898,6 +1937,25 @@ describe('BookService', () => {
|
||||
expect(onProgress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('bulkRefreshMetadata delegates each item to non-preview refreshMetadata', async () => {
|
||||
const { service, bookRepo } = makeService();
|
||||
const user = makeUser();
|
||||
const refreshSpy = vi.spyOn(service, 'refreshMetadata').mockResolvedValue({ id: 1 } as never);
|
||||
const onProgress = vi.fn();
|
||||
|
||||
bookRepo.findLibraryIdsByBookIds.mockResolvedValue([
|
||||
{ id: 1, libraryId: 7 },
|
||||
{ id: 2, libraryId: 7 },
|
||||
]);
|
||||
|
||||
const result = await service.bulkRefreshMetadata([1, 2], user, onProgress);
|
||||
|
||||
expect(result).toEqual({ processed: 2, failed: 0 });
|
||||
expect(refreshSpy).toHaveBeenCalledWith(1, false, user);
|
||||
expect(refreshSpy).toHaveBeenCalledWith(2, false, user);
|
||||
expect(onProgress).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('deleteBooks verifies access, removes book files, and removes cover directories without failing on rm errors', async () => {
|
||||
const { service, bookRepo, libraryService } = makeService();
|
||||
const user = makeUser();
|
||||
|
||||
@@ -2465,6 +2465,7 @@ export class BookService {
|
||||
if (r.seriesName !== undefined) dto.seriesName = r.seriesName as string | null;
|
||||
if (r.seriesIndex !== undefined) dto.seriesIndex = r.seriesIndex as number | null;
|
||||
if (r.seriesMemberships !== undefined) dto.seriesMemberships = r.seriesMemberships as UpdateBookMetadataDto['seriesMemberships'];
|
||||
if (r.hardcoverEditionId !== undefined) dto.hardcoverEditionId = r.hardcoverEditionId as string | null;
|
||||
if (r.narrators !== undefined || r.duration !== undefined || r.abridged !== undefined || r.chapters !== undefined) {
|
||||
dto.audioMetadata = {};
|
||||
if (r.narrators !== undefined) dto.audioMetadata.narrators = r.narrators as string[];
|
||||
|
||||
Reference in New Issue
Block a user