mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(metadata): store and write hardcover edition IDs
Adds hardcoverEditionId to the database schema and shared types, enabling it to be stored as first-class metadata. Updates the file metadata parsers and writers (EPUB, PDF, CBZ) to read and embed this ID, and exposes it in the frontend metadata editor. Closes #420
This commit is contained in:
@@ -207,6 +207,7 @@ const currentSource = computed<MetadataSource>(() => ({
|
|||||||
narrators: [],
|
narrators: [],
|
||||||
durationSeconds: null,
|
durationSeconds: null,
|
||||||
abridged: null,
|
abridged: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function openSearch() {
|
function openSearch() {
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ function makeDetail(overrides: Partial<BookDetail> = {}): BookDetail {
|
|||||||
seriesIndex: null,
|
seriesIndex: null,
|
||||||
rating: null,
|
rating: null,
|
||||||
coverSource: null,
|
coverSource: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
providerIds: {},
|
providerIds: {},
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ function makeBook(overrides: Partial<BookDetail> = {}): BookDetail {
|
|||||||
seriesIndex: null,
|
seriesIndex: null,
|
||||||
rating: null,
|
rating: null,
|
||||||
coverSource: null,
|
coverSource: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
providerIds: {},
|
providerIds: {},
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const DIRECT_PATCH_FIELDS = [
|
|||||||
'goodreadsId',
|
'goodreadsId',
|
||||||
'amazonId',
|
'amazonId',
|
||||||
'hardcoverId',
|
'hardcoverId',
|
||||||
|
'hardcoverEditionId',
|
||||||
'openLibraryId',
|
'openLibraryId',
|
||||||
'itunesId',
|
'itunesId',
|
||||||
'audibleId',
|
'audibleId',
|
||||||
@@ -525,6 +526,7 @@ function buildPreviewPatch(preview: MetadataRefreshPreview): MetadataPatch {
|
|||||||
goodreadsId: preview.goodreadsId,
|
goodreadsId: preview.goodreadsId,
|
||||||
amazonId: preview.amazonId,
|
amazonId: preview.amazonId,
|
||||||
hardcoverId: preview.hardcoverId,
|
hardcoverId: preview.hardcoverId,
|
||||||
|
hardcoverEditionId: preview.hardcoverEditionId,
|
||||||
openLibraryId: preview.openLibraryId,
|
openLibraryId: preview.openLibraryId,
|
||||||
itunesId: preview.itunesId,
|
itunesId: preview.itunesId,
|
||||||
audibleId: preview.audibleId,
|
audibleId: preview.audibleId,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const currentSource = computed<MetadataSource>(() => ({
|
|||||||
narrators: props.book.audioMetadata?.narrators.map((n) => n.name) ?? [],
|
narrators: props.book.audioMetadata?.narrators.map((n) => n.name) ?? [],
|
||||||
durationSeconds: props.book.audioMetadata?.durationSeconds ?? null,
|
durationSeconds: props.book.audioMetadata?.durationSeconds ?? null,
|
||||||
abridged: props.book.audioMetadata?.abridged ?? null,
|
abridged: props.book.audioMetadata?.abridged ?? null,
|
||||||
|
hardcoverEditionId: props.book.hardcoverEditionId,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ function makeBook(overrides: Partial<BookDetail> = {}): BookDetail {
|
|||||||
seriesIndex: null,
|
seriesIndex: null,
|
||||||
rating: null,
|
rating: null,
|
||||||
coverSource: 'extracted',
|
coverSource: 'extracted',
|
||||||
|
hardcoverEditionId: null,
|
||||||
providerIds: {},
|
providerIds: {},
|
||||||
authors: [{ id: 1, name: 'Author One', sortName: null }],
|
authors: [{ id: 1, name: 'Author One', sortName: null }],
|
||||||
genres: [],
|
genres: [],
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function makeBook(overrides = {}) {
|
|||||||
seriesIndex: null,
|
seriesIndex: null,
|
||||||
rating: null,
|
rating: null,
|
||||||
coverSource: null,
|
coverSource: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
providerIds: {},
|
providerIds: {},
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ describe('useMetadataDiff', () => {
|
|||||||
narrators: [],
|
narrators: [],
|
||||||
durationSeconds: null,
|
durationSeconds: null,
|
||||||
abridged: null,
|
abridged: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockCandidate1: MetadataCandidate = {
|
const mockCandidate1: MetadataCandidate = {
|
||||||
@@ -45,6 +46,8 @@ describe('useMetadataDiff', () => {
|
|||||||
{ key: 'goodreads' as MetadataProviderKey, label: 'Goodreads', identifiable: true },
|
{ key: 'goodreads' as MetadataProviderKey, label: 'Goodreads', identifiable: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const hardcoverProviders = [{ key: 'hardcover' as MetadataProviderKey, label: 'Hardcover', identifiable: true }]
|
||||||
|
|
||||||
it('initializes with fields from active provider', () => {
|
it('initializes with fields from active provider', () => {
|
||||||
const candidates = ref([mockCandidate1, mockCandidate2])
|
const candidates = ref([mockCandidate1, mockCandidate2])
|
||||||
const activeProvider = ref<MetadataProviderKey>('google')
|
const activeProvider = ref<MetadataProviderKey>('google')
|
||||||
@@ -105,6 +108,81 @@ describe('useMetadataDiff', () => {
|
|||||||
expect(formPatch.goodreadsId).toBe('gr1')
|
expect(formPatch.goodreadsId).toBe('gr1')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows and applies Hardcover edition IDs as lockable metadata fields', () => {
|
||||||
|
const candidate: MetadataCandidate = {
|
||||||
|
provider: 'hardcover',
|
||||||
|
providerId: 'the-name-of-the-wind',
|
||||||
|
hardcoverEditionId: '1001',
|
||||||
|
title: 'Hardcover Title',
|
||||||
|
}
|
||||||
|
const candidates = ref([candidate])
|
||||||
|
const activeProvider = ref<MetadataProviderKey>('hardcover')
|
||||||
|
const { fields, toggleField, buildPatch } = useMetadataDiff(mockCurrent, candidates, activeProvider, hardcoverProviders)
|
||||||
|
|
||||||
|
const editionField = fields.value.find((f) => f.key === 'hardcoverEditionId')
|
||||||
|
expect(editionField).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
bookValue: '',
|
||||||
|
candidateDisplay: '1001',
|
||||||
|
isLocked: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
toggleField('hardcoverEditionId')
|
||||||
|
expect(buildPatch().formPatch).toEqual({
|
||||||
|
hardcoverEditionId: '1001',
|
||||||
|
hardcoverId: 'the-name-of-the-wind',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('auto-includes unlocked Hardcover edition IDs when applying other Hardcover fields', () => {
|
||||||
|
const candidate: MetadataCandidate = {
|
||||||
|
provider: 'hardcover',
|
||||||
|
providerId: 'the-name-of-the-wind',
|
||||||
|
hardcoverEditionId: '1001',
|
||||||
|
title: 'Hardcover Title',
|
||||||
|
}
|
||||||
|
const candidates = ref([candidate])
|
||||||
|
const activeProvider = ref<MetadataProviderKey>('hardcover')
|
||||||
|
const { toggleField, buildPatch } = useMetadataDiff(mockCurrent, candidates, activeProvider, hardcoverProviders)
|
||||||
|
|
||||||
|
toggleField('title')
|
||||||
|
|
||||||
|
expect(buildPatch().formPatch).toMatchObject({
|
||||||
|
title: 'Hardcover Title',
|
||||||
|
hardcoverId: 'the-name-of-the-wind',
|
||||||
|
hardcoverEditionId: '1001',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not auto-include locked Hardcover edition IDs', () => {
|
||||||
|
const candidate: MetadataCandidate = {
|
||||||
|
provider: 'hardcover',
|
||||||
|
providerId: 'the-name-of-the-wind',
|
||||||
|
hardcoverEditionId: '1001',
|
||||||
|
title: 'Hardcover Title',
|
||||||
|
}
|
||||||
|
const candidates = ref([candidate])
|
||||||
|
const activeProvider = ref<MetadataProviderKey>('hardcover')
|
||||||
|
const { toggleField, buildPatch } = useMetadataDiff(
|
||||||
|
mockCurrent,
|
||||||
|
candidates,
|
||||||
|
activeProvider,
|
||||||
|
hardcoverProviders,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
ref(['hardcoverEditionId'] as const),
|
||||||
|
)
|
||||||
|
|
||||||
|
toggleField('title')
|
||||||
|
|
||||||
|
expect(buildPatch().formPatch).toMatchObject({
|
||||||
|
title: 'Hardcover Title',
|
||||||
|
hardcoverId: 'the-name-of-the-wind',
|
||||||
|
})
|
||||||
|
expect(buildPatch().formPatch.hardcoverEditionId).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('builds series memberships when series name and index are picked from the same provider', () => {
|
it('builds series memberships when series name and index are picked from the same provider', () => {
|
||||||
const audibleCandidate: MetadataCandidate = {
|
const audibleCandidate: MetadataCandidate = {
|
||||||
provider: 'audible',
|
provider: 'audible',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ function makeBook(overrides: Partial<BookDetail> = {}): BookDetail {
|
|||||||
seriesIndex: null,
|
seriesIndex: null,
|
||||||
rating: null,
|
rating: null,
|
||||||
coverSource: null,
|
coverSource: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
providerIds: {},
|
providerIds: {},
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
@@ -213,6 +214,25 @@ describe('useMetadataEditor', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('loads and saves Hardcover edition IDs separately from Hardcover book IDs', async () => {
|
||||||
|
const book = makeBook({ providerIds: { hardcover: 'old-book-slug' }, hardcoverEditionId: '8941973' })
|
||||||
|
apiMock.mockResolvedValue({ ok: true, json: async () => ({ ...book, hardcoverEditionId: '8941974' }) })
|
||||||
|
|
||||||
|
const { form, load, save } = useMetadataEditor()
|
||||||
|
load(book)
|
||||||
|
expect(form.hardcoverId).toBe('old-book-slug')
|
||||||
|
expect(form.hardcoverEditionId).toBe('8941973')
|
||||||
|
|
||||||
|
form.hardcoverEditionId = '8941974'
|
||||||
|
await save(book.id, [])
|
||||||
|
|
||||||
|
const [, req] = apiMock.mock.calls[0] as [string, RequestInit]
|
||||||
|
expect(JSON.parse(String(req.body))).toEqual({
|
||||||
|
metadata: { hardcoverEditionId: '8941974' },
|
||||||
|
lockedFields: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('normalizes a zero page count to null in the save payload', async () => {
|
it('normalizes a zero page count to null in the save payload', async () => {
|
||||||
const book = makeBook({ pageCount: 320 })
|
const book = makeBook({ pageCount: 320 })
|
||||||
apiMock.mockResolvedValue({ ok: true, json: async () => book })
|
apiMock.mockResolvedValue({ ok: true, json: async () => book })
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ function makeBook(overrides: Partial<BookDetail> = {}): BookDetail {
|
|||||||
seriesIndex: null,
|
seriesIndex: null,
|
||||||
rating: null,
|
rating: null,
|
||||||
coverSource: null,
|
coverSource: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
providerIds: {},
|
providerIds: {},
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface FileMetadata {
|
|||||||
goodreadsId?: string | null
|
goodreadsId?: string | null
|
||||||
amazonId?: string | null
|
amazonId?: string | null
|
||||||
hardcoverId?: string | null
|
hardcoverId?: string | null
|
||||||
|
hardcoverEditionId?: string | null
|
||||||
openLibraryId?: string | null
|
openLibraryId?: string | null
|
||||||
itunesId?: string | null
|
itunesId?: string | null
|
||||||
audibleId?: string | null
|
audibleId?: string | null
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export type DiffFieldKey =
|
|||||||
| 'durationSeconds'
|
| 'durationSeconds'
|
||||||
| 'abridged'
|
| 'abridged'
|
||||||
| 'coverUrl'
|
| 'coverUrl'
|
||||||
|
| 'hardcoverEditionId'
|
||||||
| ProviderIdPatchField
|
| ProviderIdPatchField
|
||||||
| 'sourceUrl'
|
| 'sourceUrl'
|
||||||
| ComicDiffFieldKey
|
| ComicDiffFieldKey
|
||||||
@@ -93,6 +94,7 @@ export interface MetadataPatch {
|
|||||||
goodreadsId?: string | null
|
goodreadsId?: string | null
|
||||||
amazonId?: string | null
|
amazonId?: string | null
|
||||||
hardcoverId?: string | null
|
hardcoverId?: string | null
|
||||||
|
hardcoverEditionId?: string | null
|
||||||
openLibraryId?: string | null
|
openLibraryId?: string | null
|
||||||
itunesId?: string | null
|
itunesId?: string | null
|
||||||
audibleId?: string | null
|
audibleId?: string | null
|
||||||
@@ -123,6 +125,7 @@ export const FIELD_DEFS: { key: DiffFieldKey; label: string }[] = [
|
|||||||
{ key: 'narrators', label: 'Narrators' },
|
{ key: 'narrators', label: 'Narrators' },
|
||||||
{ key: 'durationSeconds', label: 'Duration (seconds)' },
|
{ key: 'durationSeconds', label: 'Duration (seconds)' },
|
||||||
{ key: 'abridged', label: 'Abridged' },
|
{ key: 'abridged', label: 'Abridged' },
|
||||||
|
{ key: 'hardcoverEditionId', label: 'Hardcover Edition ID' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export interface ComicFieldDef {
|
export interface ComicFieldDef {
|
||||||
@@ -516,6 +519,14 @@ export function useMetadataDiff(
|
|||||||
if (idField && formPatch[idField] === undefined) {
|
if (idField && formPatch[idField] === undefined) {
|
||||||
formPatch[idField] = candidate.providerId
|
formPatch[idField] = candidate.providerId
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
provider === 'hardcover' &&
|
||||||
|
candidate.hardcoverEditionId &&
|
||||||
|
formPatch.hardcoverEditionId === undefined &&
|
||||||
|
!lockedFieldSet.value.has('hardcoverEditionId')
|
||||||
|
) {
|
||||||
|
formPatch.hardcoverEditionId = candidate.hardcoverEditionId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve existing provider IDs on the book that we aren't overwriting
|
// Preserve existing provider IDs on the book that we aren't overwriting
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const ROOT_FIELDS = [
|
|||||||
'goodreadsId',
|
'goodreadsId',
|
||||||
'amazonId',
|
'amazonId',
|
||||||
'hardcoverId',
|
'hardcoverId',
|
||||||
|
'hardcoverEditionId',
|
||||||
'openLibraryId',
|
'openLibraryId',
|
||||||
'itunesId',
|
'itunesId',
|
||||||
'audibleId',
|
'audibleId',
|
||||||
@@ -126,6 +127,7 @@ export function useMetadataEditor() {
|
|||||||
goodreadsId: null as string | null,
|
goodreadsId: null as string | null,
|
||||||
amazonId: null as string | null,
|
amazonId: null as string | null,
|
||||||
hardcoverId: null as string | null,
|
hardcoverId: null as string | null,
|
||||||
|
hardcoverEditionId: null as string | null,
|
||||||
openLibraryId: null as string | null,
|
openLibraryId: null as string | null,
|
||||||
itunesId: null as string | null,
|
itunesId: null as string | null,
|
||||||
audibleId: null as string | null,
|
audibleId: null as string | null,
|
||||||
@@ -177,6 +179,7 @@ export function useMetadataEditor() {
|
|||||||
form.goodreadsId = book.providerIds.goodreads ?? null
|
form.goodreadsId = book.providerIds.goodreads ?? null
|
||||||
form.amazonId = book.providerIds.amazon ?? null
|
form.amazonId = book.providerIds.amazon ?? null
|
||||||
form.hardcoverId = book.providerIds.hardcover ?? null
|
form.hardcoverId = book.providerIds.hardcover ?? null
|
||||||
|
form.hardcoverEditionId = book.hardcoverEditionId
|
||||||
form.openLibraryId = book.providerIds.openLibrary ?? null
|
form.openLibraryId = book.providerIds.openLibrary ?? null
|
||||||
form.itunesId = book.providerIds.itunes ?? null
|
form.itunesId = book.providerIds.itunes ?? null
|
||||||
form.audibleId = book.providerIds.audible ?? null
|
form.audibleId = book.providerIds.audible ?? null
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ describe('buildFileMetadataPatch', () => {
|
|||||||
goodreadsId: 'goodreads-1',
|
goodreadsId: 'goodreads-1',
|
||||||
amazonId: 'B00TEST',
|
amazonId: 'B00TEST',
|
||||||
hardcoverId: 'hardcover-1',
|
hardcoverId: 'hardcover-1',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
openLibraryId: 'OL123W',
|
openLibraryId: 'OL123W',
|
||||||
itunesId: 'itunes-1',
|
itunesId: 'itunes-1',
|
||||||
audibleId: 'audible-1',
|
audibleId: 'audible-1',
|
||||||
@@ -21,6 +22,7 @@ describe('buildFileMetadataPatch', () => {
|
|||||||
goodreadsId: 'goodreads-1',
|
goodreadsId: 'goodreads-1',
|
||||||
amazonId: 'B00TEST',
|
amazonId: 'B00TEST',
|
||||||
hardcoverId: 'hardcover-1',
|
hardcoverId: 'hardcover-1',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
openLibraryId: 'OL123W',
|
openLibraryId: 'OL123W',
|
||||||
itunesId: 'itunes-1',
|
itunesId: 'itunes-1',
|
||||||
audibleId: 'audible-1',
|
audibleId: 'audible-1',
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function makeBook(overrides: Partial<BookDetail> = {}): BookDetail {
|
|||||||
seriesIndex: null,
|
seriesIndex: null,
|
||||||
rating: null,
|
rating: null,
|
||||||
coverSource: null,
|
coverSource: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
providerIds: {},
|
providerIds: {},
|
||||||
authors: [{ id: 1, name: 'Hanya Yanagihara', sortName: null }],
|
authors: [{ id: 1, name: 'Hanya Yanagihara', sortName: null }],
|
||||||
genres: [],
|
genres: [],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ describe('provider ID fields', () => {
|
|||||||
'goodreadsId',
|
'goodreadsId',
|
||||||
'amazonId',
|
'amazonId',
|
||||||
'hardcoverId',
|
'hardcoverId',
|
||||||
|
'hardcoverEditionId',
|
||||||
'openLibraryId',
|
'openLibraryId',
|
||||||
'itunesId',
|
'itunesId',
|
||||||
'audibleId',
|
'audibleId',
|
||||||
@@ -25,15 +26,20 @@ describe('provider ID fields', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('filters provider ID fields to the effective provider set', () => {
|
it('filters provider ID fields to the effective provider set', () => {
|
||||||
const fields = filterProviderIdFields([provider(MetadataProviderKey.KOBO), provider(MetadataProviderKey.COMICVINE)])
|
const fields = filterProviderIdFields([
|
||||||
|
provider(MetadataProviderKey.HARDCOVER),
|
||||||
|
provider(MetadataProviderKey.KOBO),
|
||||||
|
provider(MetadataProviderKey.COMICVINE),
|
||||||
|
])
|
||||||
|
|
||||||
expect(fields.map((field) => field.field)).toEqual(['koboId', 'comicvineId'])
|
expect(fields.map((field) => field.field)).toEqual(['hardcoverId', 'hardcoverEditionId', 'koboId', 'comicvineId'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('reports provider ID field availability by mapped provider key', () => {
|
it('reports provider ID field availability by mapped provider key', () => {
|
||||||
const providers = [provider(MetadataProviderKey.GOOGLE)]
|
const providers = [provider(MetadataProviderKey.GOOGLE)]
|
||||||
|
|
||||||
expect(isProviderIdFieldAvailable('googleBooksId', providers)).toBe(true)
|
expect(isProviderIdFieldAvailable('googleBooksId', providers)).toBe(true)
|
||||||
|
expect(isProviderIdFieldAvailable('hardcoverEditionId', providers)).toBe(false)
|
||||||
expect(isProviderIdFieldAvailable('koboId', providers)).toBe(false)
|
expect(isProviderIdFieldAvailable('koboId', providers)).toBe(false)
|
||||||
expect(isProviderIdFieldAvailable('futureProviderId' as ProviderIdFormField, providers)).toBe(true)
|
expect(isProviderIdFieldAvailable('futureProviderId' as ProviderIdFormField, providers)).toBe(true)
|
||||||
expect(isProviderIdFormField('googleBooksId')).toBe(true)
|
expect(isProviderIdFormField('googleBooksId')).toBe(true)
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ function makeDetail(overrides: Partial<BookDetail> = {}): BookDetail {
|
|||||||
seriesMemberships: [],
|
seriesMemberships: [],
|
||||||
rating: null,
|
rating: null,
|
||||||
coverSource: null,
|
coverSource: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
providerIds: {},
|
providerIds: {},
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
@@ -76,6 +77,8 @@ describe('mergeBookCardWithDetail', () => {
|
|||||||
publishedYear: 2021,
|
publishedYear: 2021,
|
||||||
language: 'en',
|
language: 'en',
|
||||||
pageCount: 350,
|
pageCount: 350,
|
||||||
|
providerIds: { hardcover: 'new-book-slug' },
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
seriesName: 'My Series',
|
seriesName: 'My Series',
|
||||||
seriesIndex: 2,
|
seriesIndex: 2,
|
||||||
rating: 8,
|
rating: 8,
|
||||||
@@ -92,6 +95,8 @@ describe('mergeBookCardWithDetail', () => {
|
|||||||
expect(result.publishedYear).toBe(2021)
|
expect(result.publishedYear).toBe(2021)
|
||||||
expect(result.language).toBe('en')
|
expect(result.language).toBe('en')
|
||||||
expect(result.pageCount).toBe(350)
|
expect(result.pageCount).toBe(350)
|
||||||
|
expect(result.hardcoverId).toBe('new-book-slug')
|
||||||
|
expect(result.hardcoverEditionId).toBe('8941973')
|
||||||
expect(result.seriesName).toBe('My Series')
|
expect(result.seriesName).toBe('My Series')
|
||||||
expect(result.seriesIndex).toBe(2)
|
expect(result.seriesIndex).toBe(2)
|
||||||
expect(result.rating).toBe(8)
|
expect(result.rating).toBe(8)
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export function mergeBookCardWithDetail(book: BookCard, detail: BookDetail): Boo
|
|||||||
publishedYear: detail.publishedYear,
|
publishedYear: detail.publishedYear,
|
||||||
language: detail.language,
|
language: detail.language,
|
||||||
pageCount: detail.pageCount,
|
pageCount: detail.pageCount,
|
||||||
|
hardcoverId: detail.providerIds.hardcover ?? null,
|
||||||
|
hardcoverEditionId: detail.hardcoverEditionId,
|
||||||
seriesName: detail.seriesName,
|
seriesName: detail.seriesName,
|
||||||
seriesIndex: detail.seriesIndex,
|
seriesIndex: detail.seriesIndex,
|
||||||
seriesMemberships: detail.seriesMemberships,
|
seriesMemberships: detail.seriesMemberships,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const FILE_METADATA_PATCH_FIELDS = [
|
|||||||
'goodreadsId',
|
'goodreadsId',
|
||||||
'amazonId',
|
'amazonId',
|
||||||
'hardcoverId',
|
'hardcoverId',
|
||||||
|
'hardcoverEditionId',
|
||||||
'openLibraryId',
|
'openLibraryId',
|
||||||
'itunesId',
|
'itunesId',
|
||||||
'audibleId',
|
'audibleId',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const PROVIDER_ID_FIELDS = [
|
|||||||
{ provider: MetadataProviderKey.GOODREADS, field: 'goodreadsId', label: 'Goodreads' },
|
{ provider: MetadataProviderKey.GOODREADS, field: 'goodreadsId', label: 'Goodreads' },
|
||||||
{ provider: MetadataProviderKey.AMAZON, field: 'amazonId', label: 'Amazon' },
|
{ provider: MetadataProviderKey.AMAZON, field: 'amazonId', label: 'Amazon' },
|
||||||
{ provider: MetadataProviderKey.HARDCOVER, field: 'hardcoverId', label: 'Hardcover' },
|
{ provider: MetadataProviderKey.HARDCOVER, field: 'hardcoverId', label: 'Hardcover' },
|
||||||
|
{ provider: MetadataProviderKey.HARDCOVER, field: 'hardcoverEditionId', label: 'Hardcover Edition' },
|
||||||
{ provider: MetadataProviderKey.OPEN_LIBRARY, field: 'openLibraryId', label: 'OpenLibrary' },
|
{ provider: MetadataProviderKey.OPEN_LIBRARY, field: 'openLibraryId', label: 'OpenLibrary' },
|
||||||
{ provider: MetadataProviderKey.ITUNES, field: 'itunesId', label: 'iTunes' },
|
{ provider: MetadataProviderKey.ITUNES, field: 'itunesId', label: 'iTunes' },
|
||||||
{ provider: MetadataProviderKey.AUDIBLE, field: 'audibleId', label: 'Audible' },
|
{ provider: MetadataProviderKey.AUDIBLE, field: 'audibleId', label: 'Audible' },
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ function makeBook(overrides: Partial<BookDetail> = {}): BookDetail {
|
|||||||
seriesIndex: null,
|
seriesIndex: null,
|
||||||
rating: 2,
|
rating: 2,
|
||||||
coverSource: null,
|
coverSource: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
providerIds: {},
|
providerIds: {},
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ export type BookCard = {
|
|||||||
publisher: string | null;
|
publisher: string | null;
|
||||||
pageCount: number | null;
|
pageCount: number | null;
|
||||||
isbn13: string | null;
|
isbn13: string | null;
|
||||||
|
hardcoverId?: string | null;
|
||||||
|
hardcoverEditionId?: string | null;
|
||||||
narrators: string[];
|
narrators: string[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
collapsedSeries?: import("./series-collapse").CollapsedSeriesInfo;
|
collapsedSeries?: import("./series-collapse").CollapsedSeriesInfo;
|
||||||
@@ -132,6 +134,7 @@ export type BookDetail = {
|
|||||||
seriesMemberships?: BookSeriesMembership[];
|
seriesMemberships?: BookSeriesMembership[];
|
||||||
rating: number | null;
|
rating: number | null;
|
||||||
coverSource: "extracted" | "custom" | null;
|
coverSource: "extracted" | "custom" | null;
|
||||||
|
hardcoverEditionId: string | null;
|
||||||
providerIds: ProviderIds;
|
providerIds: ProviderIds;
|
||||||
authors: { id: number; name: string; sortName: string | null }[];
|
authors: { id: number; name: string; sortName: string | null }[];
|
||||||
genres: string[];
|
genres: string[];
|
||||||
@@ -173,6 +176,7 @@ export type BookMetadataRefreshPreviewFields = {
|
|||||||
goodreadsId?: string | null;
|
goodreadsId?: string | null;
|
||||||
amazonId?: string | null;
|
amazonId?: string | null;
|
||||||
hardcoverId?: string | null;
|
hardcoverId?: string | null;
|
||||||
|
hardcoverEditionId?: string | null;
|
||||||
openLibraryId?: string | null;
|
openLibraryId?: string | null;
|
||||||
itunesId?: string | null;
|
itunesId?: string | null;
|
||||||
audibleId?: string | null;
|
audibleId?: string | null;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const COMMON_PROVIDER_BOOK_FILE_WRITE_FIELDS = [
|
|||||||
"goodreadsId",
|
"goodreadsId",
|
||||||
"amazonId",
|
"amazonId",
|
||||||
"hardcoverId",
|
"hardcoverId",
|
||||||
|
"hardcoverEditionId",
|
||||||
"openLibraryId",
|
"openLibraryId",
|
||||||
"ranobedbId",
|
"ranobedbId",
|
||||||
"koboId",
|
"koboId",
|
||||||
@@ -74,6 +75,7 @@ export const BOOK_FILE_WRITE_FIELD_LABELS = {
|
|||||||
goodreadsId: "Goodreads ID",
|
goodreadsId: "Goodreads ID",
|
||||||
amazonId: "Amazon ID",
|
amazonId: "Amazon ID",
|
||||||
hardcoverId: "Hardcover ID",
|
hardcoverId: "Hardcover ID",
|
||||||
|
hardcoverEditionId: "Hardcover edition ID",
|
||||||
openLibraryId: "Open Library ID",
|
openLibraryId: "Open Library ID",
|
||||||
ranobedbId: "RanobeDB ID",
|
ranobedbId: "RanobeDB ID",
|
||||||
koboId: "Kobo ID",
|
koboId: "Kobo ID",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface MetadataSeriesMembership {
|
|||||||
export interface MetadataCandidate {
|
export interface MetadataCandidate {
|
||||||
provider: MetadataProviderKey;
|
provider: MetadataProviderKey;
|
||||||
providerId: string;
|
providerId: string;
|
||||||
|
hardcoverEditionId?: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
authors?: string[];
|
authors?: string[];
|
||||||
@@ -114,4 +115,5 @@ export interface MetadataSource {
|
|||||||
narrators: string[];
|
narrators: string[];
|
||||||
durationSeconds: number | null;
|
durationSeconds: number | null;
|
||||||
abridged: boolean | null;
|
abridged: boolean | null;
|
||||||
|
hardcoverEditionId: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const BOOK_METADATA_LOCK_FIELDS = [
|
|||||||
"goodreadsId",
|
"goodreadsId",
|
||||||
"amazonId",
|
"amazonId",
|
||||||
"hardcoverId",
|
"hardcoverId",
|
||||||
|
"hardcoverEditionId",
|
||||||
"openLibraryId",
|
"openLibraryId",
|
||||||
"itunesId",
|
"itunesId",
|
||||||
"audibleId",
|
"audibleId",
|
||||||
@@ -82,6 +83,7 @@ export const BOOK_METADATA_LOCK_GROUPS: BookMetadataLockGroup[] = [
|
|||||||
"goodreadsId",
|
"goodreadsId",
|
||||||
"amazonId",
|
"amazonId",
|
||||||
"hardcoverId",
|
"hardcoverId",
|
||||||
|
"hardcoverEditionId",
|
||||||
"openLibraryId",
|
"openLibraryId",
|
||||||
"itunesId",
|
"itunesId",
|
||||||
"audibleId",
|
"audibleId",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "book_metadata" ADD COLUMN "hardcover_edition_id" varchar(50);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -176,6 +176,13 @@
|
|||||||
"when": 1782366176669,
|
"when": 1782366176669,
|
||||||
"tag": "0024_custom_metadata",
|
"tag": "0024_custom_metadata",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 25,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782412211738,
|
||||||
|
"tag": "0025_add_hardcover_edition_id",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export const bookMetadata = pgTable(
|
|||||||
goodreadsId: varchar('goodreads_id', { length: 50 }),
|
goodreadsId: varchar('goodreads_id', { length: 50 }),
|
||||||
amazonId: varchar('amazon_id', { length: 20 }),
|
amazonId: varchar('amazon_id', { length: 20 }),
|
||||||
hardcoverId: varchar('hardcover_id', { length: 255 }),
|
hardcoverId: varchar('hardcover_id', { length: 255 }),
|
||||||
|
hardcoverEditionId: varchar('hardcover_edition_id', { length: 50 }),
|
||||||
openLibraryId: varchar('open_library_id', { length: 50 }),
|
openLibraryId: varchar('open_library_id', { length: 50 }),
|
||||||
itunesId: varchar('itunes_id', { length: 50 }),
|
itunesId: varchar('itunes_id', { length: 50 }),
|
||||||
koboId: varchar('kobo_id', { length: 255 }),
|
koboId: varchar('kobo_id', { length: 255 }),
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ describe('BookDockMetadataService', () => {
|
|||||||
goodreadsId: null,
|
goodreadsId: null,
|
||||||
amazonId: null,
|
amazonId: null,
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
openLibraryId: null,
|
openLibraryId: null,
|
||||||
itunesId: null,
|
itunesId: null,
|
||||||
coverBuffer: Buffer.from('cover'),
|
coverBuffer: Buffer.from('cover'),
|
||||||
@@ -183,6 +184,7 @@ describe('BookDockMetadataService', () => {
|
|||||||
goodreadsId: null,
|
goodreadsId: null,
|
||||||
amazonId: null,
|
amazonId: null,
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
openLibraryId: null,
|
openLibraryId: null,
|
||||||
itunesId: null,
|
itunesId: null,
|
||||||
coverBuffer: null,
|
coverBuffer: null,
|
||||||
|
|||||||
+4
@@ -255,6 +255,7 @@ describe('BookMetadataFetchOrchestratorService', () => {
|
|||||||
seriesIndex: 2,
|
seriesIndex: 2,
|
||||||
duration: 3600,
|
duration: 3600,
|
||||||
abridged: false,
|
abridged: false,
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
chapters: [{ title: 'Ch 1', startMs: 0 }],
|
chapters: [{ title: 'Ch 1', startMs: 0 }],
|
||||||
authors: ['Author A'],
|
authors: ['Author A'],
|
||||||
genres: ['Genre A'],
|
genres: ['Genre A'],
|
||||||
@@ -264,6 +265,7 @@ describe('BookMetadataFetchOrchestratorService', () => {
|
|||||||
},
|
},
|
||||||
providerIds: {
|
providerIds: {
|
||||||
[MetadataProviderKey.GOOGLE]: 'g1',
|
[MetadataProviderKey.GOOGLE]: 'g1',
|
||||||
|
[MetadataProviderKey.HARDCOVER]: 'new-orleans-rush',
|
||||||
[MetadataProviderKey.AUDIBLE]: 'a1',
|
[MetadataProviderKey.AUDIBLE]: 'a1',
|
||||||
[MetadataProviderKey.KOBO]: 'kobo-1',
|
[MetadataProviderKey.KOBO]: 'kobo-1',
|
||||||
},
|
},
|
||||||
@@ -281,6 +283,8 @@ describe('BookMetadataFetchOrchestratorService', () => {
|
|||||||
durationSeconds: 3600,
|
durationSeconds: 3600,
|
||||||
abridged: false,
|
abridged: false,
|
||||||
googleBooksId: 'g1',
|
googleBooksId: 'g1',
|
||||||
|
hardcoverId: 'new-orleans-rush',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
audibleId: 'a1',
|
audibleId: 'a1',
|
||||||
koboId: 'kobo-1',
|
koboId: 'kobo-1',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst
|
|||||||
if (filteredProviderIds[MetadataProviderKey.GOODREADS]) scalarFields.goodreadsId = filteredProviderIds[MetadataProviderKey.GOODREADS];
|
if (filteredProviderIds[MetadataProviderKey.GOODREADS]) scalarFields.goodreadsId = filteredProviderIds[MetadataProviderKey.GOODREADS];
|
||||||
if (filteredProviderIds[MetadataProviderKey.AMAZON]) scalarFields.amazonId = filteredProviderIds[MetadataProviderKey.AMAZON];
|
if (filteredProviderIds[MetadataProviderKey.AMAZON]) scalarFields.amazonId = filteredProviderIds[MetadataProviderKey.AMAZON];
|
||||||
if (filteredProviderIds[MetadataProviderKey.HARDCOVER]) scalarFields.hardcoverId = filteredProviderIds[MetadataProviderKey.HARDCOVER];
|
if (filteredProviderIds[MetadataProviderKey.HARDCOVER]) scalarFields.hardcoverId = filteredProviderIds[MetadataProviderKey.HARDCOVER];
|
||||||
|
const hardcoverEditionId = this.asNullableString(filteredResolved.hardcoverEditionId);
|
||||||
|
if (hardcoverEditionId !== undefined) scalarFields.hardcoverEditionId = hardcoverEditionId;
|
||||||
if (filteredProviderIds[MetadataProviderKey.OPEN_LIBRARY]) scalarFields.openLibraryId = filteredProviderIds[MetadataProviderKey.OPEN_LIBRARY];
|
if (filteredProviderIds[MetadataProviderKey.OPEN_LIBRARY]) scalarFields.openLibraryId = filteredProviderIds[MetadataProviderKey.OPEN_LIBRARY];
|
||||||
if (filteredProviderIds[MetadataProviderKey.ITUNES]) scalarFields.itunesId = filteredProviderIds[MetadataProviderKey.ITUNES];
|
if (filteredProviderIds[MetadataProviderKey.ITUNES]) scalarFields.itunesId = filteredProviderIds[MetadataProviderKey.ITUNES];
|
||||||
if (filteredProviderIds[MetadataProviderKey.AUDIBLE]) scalarFields.audibleId = filteredProviderIds[MetadataProviderKey.AUDIBLE];
|
if (filteredProviderIds[MetadataProviderKey.AUDIBLE]) scalarFields.audibleId = filteredProviderIds[MetadataProviderKey.AUDIBLE];
|
||||||
@@ -372,6 +374,7 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst
|
|||||||
goodreadsId?: string | null;
|
goodreadsId?: string | null;
|
||||||
amazonId?: string | null;
|
amazonId?: string | null;
|
||||||
hardcoverId?: string | null;
|
hardcoverId?: string | null;
|
||||||
|
hardcoverEditionId?: string | null;
|
||||||
openLibraryId?: string | null;
|
openLibraryId?: string | null;
|
||||||
itunesId?: string | null;
|
itunesId?: string | null;
|
||||||
audibleId?: string | null;
|
audibleId?: string | null;
|
||||||
|
|||||||
@@ -48,12 +48,13 @@ describe('BookMetadataLockService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('filters automated dto updates and preserves unlocked chapters', async () => {
|
it('filters automated dto updates and preserves unlocked chapters', async () => {
|
||||||
const { service } = makeService(['title', 'narrators', 'comicIssueNumber', 'googleBooksId']);
|
const { service } = makeService(['title', 'narrators', 'comicIssueNumber', 'googleBooksId', 'hardcoverEditionId']);
|
||||||
|
|
||||||
const result = await service.filterAutomatedBookUpdate(12, {
|
const result = await service.filterAutomatedBookUpdate(12, {
|
||||||
title: 'Locked title',
|
title: 'Locked title',
|
||||||
authors: ['Allowed Author'],
|
authors: ['Allowed Author'],
|
||||||
googleBooksId: 'g-id',
|
googleBooksId: 'g-id',
|
||||||
|
hardcoverEditionId: '1001',
|
||||||
audioMetadata: {
|
audioMetadata: {
|
||||||
narrators: ['Locked Narrator'],
|
narrators: ['Locked Narrator'],
|
||||||
chapters: [{ title: 'Chapter 1', startMs: 0 }],
|
chapters: [{ title: 'Chapter 1', startMs: 0 }],
|
||||||
@@ -73,7 +74,7 @@ describe('BookMetadataLockService', () => {
|
|||||||
volumeName: 'Allowed Volume',
|
volumeName: 'Allowed Volume',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(result.skippedFields).toEqual(['title', 'narrators', 'googleBooksId', 'comicIssueNumber']);
|
expect(result.skippedFields).toEqual(['title', 'narrators', 'googleBooksId', 'hardcoverEditionId', 'comicIssueNumber']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('assertFieldsUnlocked passes when none of the checked fields are locked', async () => {
|
it('assertFieldsUnlocked passes when none of the checked fields are locked', async () => {
|
||||||
@@ -135,13 +136,14 @@ describe('BookMetadataLockService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('filters resolved metadata and provider ids for locked fields', async () => {
|
it('filters resolved metadata and provider ids for locked fields', async () => {
|
||||||
const { service } = makeService(['cover', 'authors', 'openLibraryId', 'comicVolumeName']);
|
const { service } = makeService(['cover', 'authors', 'openLibraryId', 'hardcoverEditionId', 'comicVolumeName']);
|
||||||
|
|
||||||
const result = await service.filterResolvedMetadata(
|
const result = await service.filterResolvedMetadata(
|
||||||
12,
|
12,
|
||||||
{
|
{
|
||||||
title: 'Allowed Title',
|
title: 'Allowed Title',
|
||||||
authors: ['Locked Author'],
|
authors: ['Locked Author'],
|
||||||
|
hardcoverEditionId: '1001',
|
||||||
coverUrl: 'https://example.com/cover.jpg',
|
coverUrl: 'https://example.com/cover.jpg',
|
||||||
comicMetadata: {
|
comicMetadata: {
|
||||||
issueNumber: '7',
|
issueNumber: '7',
|
||||||
@@ -163,6 +165,6 @@ describe('BookMetadataLockService', () => {
|
|||||||
expect(result.providerIds).toEqual({
|
expect(result.providerIds).toEqual({
|
||||||
[MetadataProviderKey.GOOGLE]: 'g-id',
|
[MetadataProviderKey.GOOGLE]: 'g-id',
|
||||||
});
|
});
|
||||||
expect(result.skippedFields).toEqual(['authors', 'openLibraryId', 'comicVolumeName', 'cover']);
|
expect(result.skippedFields).toEqual(['authors', 'hardcoverEditionId', 'openLibraryId', 'comicVolumeName', 'cover']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export class BookMetadataLockService {
|
|||||||
this.copyResolvedField(filteredResolved, resolved, 'narrators', 'narrators', lockedSet, skippedFields);
|
this.copyResolvedField(filteredResolved, resolved, 'narrators', 'narrators', lockedSet, skippedFields);
|
||||||
this.copyResolvedField(filteredResolved, resolved, 'duration', 'durationSeconds', lockedSet, skippedFields);
|
this.copyResolvedField(filteredResolved, resolved, 'duration', 'durationSeconds', lockedSet, skippedFields);
|
||||||
this.copyResolvedField(filteredResolved, resolved, 'abridged', 'abridged', lockedSet, skippedFields);
|
this.copyResolvedField(filteredResolved, resolved, 'abridged', 'abridged', lockedSet, skippedFields);
|
||||||
|
this.copyResolvedField(filteredResolved, resolved, 'hardcoverEditionId', 'hardcoverEditionId', lockedSet, skippedFields);
|
||||||
|
|
||||||
if (resolved.chapters !== undefined) {
|
if (resolved.chapters !== undefined) {
|
||||||
filteredResolved.chapters = resolved.chapters;
|
filteredResolved.chapters = resolved.chapters;
|
||||||
@@ -246,6 +247,7 @@ export class BookMetadataLockService {
|
|||||||
this.copyUpdateField(filteredDto, dto, 'goodreadsId', 'goodreadsId', lockedSet, skippedFields);
|
this.copyUpdateField(filteredDto, dto, 'goodreadsId', 'goodreadsId', lockedSet, skippedFields);
|
||||||
this.copyUpdateField(filteredDto, dto, 'amazonId', 'amazonId', lockedSet, skippedFields);
|
this.copyUpdateField(filteredDto, dto, 'amazonId', 'amazonId', lockedSet, skippedFields);
|
||||||
this.copyUpdateField(filteredDto, dto, 'hardcoverId', 'hardcoverId', lockedSet, skippedFields);
|
this.copyUpdateField(filteredDto, dto, 'hardcoverId', 'hardcoverId', lockedSet, skippedFields);
|
||||||
|
this.copyUpdateField(filteredDto, dto, 'hardcoverEditionId', 'hardcoverEditionId', lockedSet, skippedFields);
|
||||||
this.copyUpdateField(filteredDto, dto, 'openLibraryId', 'openLibraryId', lockedSet, skippedFields);
|
this.copyUpdateField(filteredDto, dto, 'openLibraryId', 'openLibraryId', lockedSet, skippedFields);
|
||||||
this.copyUpdateField(filteredDto, dto, 'itunesId', 'itunesId', lockedSet, skippedFields);
|
this.copyUpdateField(filteredDto, dto, 'itunesId', 'itunesId', lockedSet, skippedFields);
|
||||||
this.copyUpdateField(filteredDto, dto, 'audibleId', 'audibleId', lockedSet, skippedFields);
|
this.copyUpdateField(filteredDto, dto, 'audibleId', 'audibleId', lockedSet, skippedFields);
|
||||||
@@ -377,6 +379,7 @@ export class BookMetadataLockService {
|
|||||||
this.addFieldIfPresent(fields, dto, 'goodreadsId', 'goodreadsId');
|
this.addFieldIfPresent(fields, dto, 'goodreadsId', 'goodreadsId');
|
||||||
this.addFieldIfPresent(fields, dto, 'amazonId', 'amazonId');
|
this.addFieldIfPresent(fields, dto, 'amazonId', 'amazonId');
|
||||||
this.addFieldIfPresent(fields, dto, 'hardcoverId', 'hardcoverId');
|
this.addFieldIfPresent(fields, dto, 'hardcoverId', 'hardcoverId');
|
||||||
|
this.addFieldIfPresent(fields, dto, 'hardcoverEditionId', 'hardcoverEditionId');
|
||||||
this.addFieldIfPresent(fields, dto, 'openLibraryId', 'openLibraryId');
|
this.addFieldIfPresent(fields, dto, 'openLibraryId', 'openLibraryId');
|
||||||
this.addFieldIfPresent(fields, dto, 'itunesId', 'itunesId');
|
this.addFieldIfPresent(fields, dto, 'itunesId', 'itunesId');
|
||||||
this.addFieldIfPresent(fields, dto, 'audibleId', 'audibleId');
|
this.addFieldIfPresent(fields, dto, 'audibleId', 'audibleId');
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ describe('BookRepository', () => {
|
|||||||
|
|
||||||
const result = await repo.findCards({ where: undefined as never, orderBy: [] as never, limit: 25, offset: 0, userId: 7 });
|
const result = await repo.findCards({ where: undefined as never, orderBy: [] as never, limit: 25, offset: 0, userId: 7 });
|
||||||
|
|
||||||
|
expect(db.select).toHaveBeenNthCalledWith(1, expect.objectContaining({ hardcoverId: expect.anything(), hardcoverEditionId: expect.anything() }));
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
rows,
|
rows,
|
||||||
authorRows,
|
authorRows,
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ type CollapsedRawRow = {
|
|||||||
locked_fields: string[] | null;
|
locked_fields: string[] | null;
|
||||||
subtitle: string | null;
|
subtitle: string | null;
|
||||||
isbn13: string | null;
|
isbn13: string | null;
|
||||||
|
hardcover_id: string | null;
|
||||||
|
hardcover_edition_id: string | null;
|
||||||
publisher: string | null;
|
publisher: string | null;
|
||||||
page_count: number | null;
|
page_count: number | null;
|
||||||
sort_title: string | null;
|
sort_title: string | null;
|
||||||
@@ -167,6 +169,8 @@ export class BookRepository {
|
|||||||
publisher: bookMetadata.publisher,
|
publisher: bookMetadata.publisher,
|
||||||
pageCount: bookMetadata.pageCount,
|
pageCount: bookMetadata.pageCount,
|
||||||
isbn13: bookMetadata.isbn13,
|
isbn13: bookMetadata.isbn13,
|
||||||
|
hardcoverId: bookMetadata.hardcoverId,
|
||||||
|
hardcoverEditionId: bookMetadata.hardcoverEditionId,
|
||||||
metadataScore: bookMetadata.metadataScore,
|
metadataScore: bookMetadata.metadataScore,
|
||||||
_total: sql<number>`count(*) over()`.as('_total'),
|
_total: sql<number>`count(*) over()`.as('_total'),
|
||||||
})
|
})
|
||||||
@@ -356,6 +360,8 @@ export class BookRepository {
|
|||||||
publisher: string | null;
|
publisher: string | null;
|
||||||
pageCount: number | null;
|
pageCount: number | null;
|
||||||
isbn13: string | null;
|
isbn13: string | null;
|
||||||
|
hardcoverId: string | null;
|
||||||
|
hardcoverEditionId: string | null;
|
||||||
bookCount: number | null;
|
bookCount: number | null;
|
||||||
readCount: number | null;
|
readCount: number | null;
|
||||||
coverBookIds: number[] | null;
|
coverBookIds: number[] | null;
|
||||||
@@ -409,6 +415,8 @@ export class BookRepository {
|
|||||||
book_metadata.page_count,
|
book_metadata.page_count,
|
||||||
book_metadata.subtitle,
|
book_metadata.subtitle,
|
||||||
book_metadata.isbn13,
|
book_metadata.isbn13,
|
||||||
|
book_metadata.hardcover_id,
|
||||||
|
book_metadata.hardcover_edition_id,
|
||||||
book_metadata.metadata_score,
|
book_metadata.metadata_score,
|
||||||
NULLIF(lower(btrim(book_metadata.series_name)), '') AS norm_series
|
NULLIF(lower(btrim(book_metadata.series_name)), '') AS norm_series
|
||||||
FROM books
|
FROM books
|
||||||
@@ -538,6 +546,8 @@ export class BookRepository {
|
|||||||
base.page_count,
|
base.page_count,
|
||||||
base.subtitle,
|
base.subtitle,
|
||||||
base.isbn13,
|
base.isbn13,
|
||||||
|
base.hardcover_id,
|
||||||
|
base.hardcover_edition_id,
|
||||||
base.metadata_score,
|
base.metadata_score,
|
||||||
base.primary_author_sort_name AS author_sort_name,
|
base.primary_author_sort_name AS author_sort_name,
|
||||||
COALESCE(base.norm_series, lower(base.title)) AS sort_title,
|
COALESCE(base.norm_series, lower(base.title)) AS sort_title,
|
||||||
@@ -602,6 +612,8 @@ export class BookRepository {
|
|||||||
publisher: r.publisher,
|
publisher: r.publisher,
|
||||||
pageCount: r.page_count !== null ? Number(r.page_count) : null,
|
pageCount: r.page_count !== null ? Number(r.page_count) : null,
|
||||||
isbn13: r.isbn13,
|
isbn13: r.isbn13,
|
||||||
|
hardcoverId: r.hardcover_id,
|
||||||
|
hardcoverEditionId: r.hardcover_edition_id,
|
||||||
bookCount: r.book_count !== null ? Number(r.book_count) : null,
|
bookCount: r.book_count !== null ? Number(r.book_count) : null,
|
||||||
readCount: r.read_count !== null ? Number(r.read_count) : null,
|
readCount: r.read_count !== null ? Number(r.read_count) : null,
|
||||||
coverBookIds: r.cover_book_ids,
|
coverBookIds: r.cover_book_ids,
|
||||||
|
|||||||
@@ -327,6 +327,8 @@ function makeBookCard(id: number, overrides?: Record<string, unknown>) {
|
|||||||
publisher: 'Pub',
|
publisher: 'Pub',
|
||||||
pageCount: 320,
|
pageCount: 320,
|
||||||
isbn13: '9780000000000',
|
isbn13: '9780000000000',
|
||||||
|
hardcoverId: 'hardcover-book-slug',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
narrators: [],
|
narrators: [],
|
||||||
...(overrides ?? {}),
|
...(overrides ?? {}),
|
||||||
};
|
};
|
||||||
@@ -629,6 +631,8 @@ describe('BookService', () => {
|
|||||||
expect(exported.content).toContain('\uFEFF');
|
expect(exported.content).toContain('\uFEFF');
|
||||||
expect(exported.content).toContain('# schemaVersion=1');
|
expect(exported.content).toContain('# schemaVersion=1');
|
||||||
expect(exported.content).toContain('bookId,libraryId,libraryName,status,title');
|
expect(exported.content).toContain('bookId,libraryId,libraryName,status,title');
|
||||||
|
expect(exported.content).toContain('isbn13,hardcoverId,hardcoverEditionId,genres');
|
||||||
|
expect(exported.content).toContain('9780000000000,hardcover-book-slug,8941973');
|
||||||
expect(exported.content).toContain('1,5,Main Library,present,Book 1');
|
expect(exported.content).toContain('1,5,Main Library,present,Book 1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ export class BookService {
|
|||||||
goodreadsId?: string | null;
|
goodreadsId?: string | null;
|
||||||
amazonId?: string | null;
|
amazonId?: string | null;
|
||||||
hardcoverId?: string | null;
|
hardcoverId?: string | null;
|
||||||
|
hardcoverEditionId?: string | null;
|
||||||
openLibraryId?: string | null;
|
openLibraryId?: string | null;
|
||||||
itunesId?: string | null;
|
itunesId?: string | null;
|
||||||
audibleId?: string | null;
|
audibleId?: string | null;
|
||||||
@@ -309,6 +310,7 @@ export class BookService {
|
|||||||
| 'goodreadsId'
|
| 'goodreadsId'
|
||||||
| 'amazonId'
|
| 'amazonId'
|
||||||
| 'hardcoverId'
|
| 'hardcoverId'
|
||||||
|
| 'hardcoverEditionId'
|
||||||
| 'openLibraryId'
|
| 'openLibraryId'
|
||||||
| 'itunesId'
|
| 'itunesId'
|
||||||
| 'audibleId'
|
| 'audibleId'
|
||||||
@@ -354,6 +356,7 @@ export class BookService {
|
|||||||
if (r.seriesIndex !== undefined) preview.seriesIndex = r.seriesIndex as number | null;
|
if (r.seriesIndex !== undefined) preview.seriesIndex = r.seriesIndex as number | null;
|
||||||
if (r.seriesMemberships !== undefined) preview.seriesMemberships = r.seriesMemberships as BookMetadataRefreshPreviewFields['seriesMemberships'];
|
if (r.seriesMemberships !== undefined) preview.seriesMemberships = r.seriesMemberships as BookMetadataRefreshPreviewFields['seriesMemberships'];
|
||||||
if (r.coverUrl !== undefined) preview.coverUrl = r.coverUrl as string;
|
if (r.coverUrl !== undefined) preview.coverUrl = r.coverUrl as string;
|
||||||
|
if (r.hardcoverEditionId !== undefined) preview.hardcoverEditionId = r.hardcoverEditionId as string | null;
|
||||||
if (r.comicMetadata !== undefined) preview.comicMetadata = r.comicMetadata as BookMetadataRefreshPreviewFields['comicMetadata'];
|
if (r.comicMetadata !== undefined) preview.comicMetadata = r.comicMetadata as BookMetadataRefreshPreviewFields['comicMetadata'];
|
||||||
|
|
||||||
if (r.narrators !== undefined || r.duration !== undefined || r.abridged !== undefined || r.chapters !== undefined) {
|
if (r.narrators !== undefined || r.duration !== undefined || r.abridged !== undefined || r.chapters !== undefined) {
|
||||||
@@ -697,6 +700,8 @@ export class BookService {
|
|||||||
'publisher',
|
'publisher',
|
||||||
'pageCount',
|
'pageCount',
|
||||||
'isbn13',
|
'isbn13',
|
||||||
|
'hardcoverId',
|
||||||
|
'hardcoverEditionId',
|
||||||
'genres',
|
'genres',
|
||||||
'tags',
|
'tags',
|
||||||
'narrators',
|
'narrators',
|
||||||
@@ -850,6 +855,8 @@ export class BookService {
|
|||||||
publisher: row.publisher,
|
publisher: row.publisher,
|
||||||
pageCount: row.pageCount,
|
pageCount: row.pageCount,
|
||||||
isbn13: row.isbn13,
|
isbn13: row.isbn13,
|
||||||
|
hardcoverId: row.hardcoverId ?? null,
|
||||||
|
hardcoverEditionId: row.hardcoverEditionId ?? null,
|
||||||
genres: row.genres,
|
genres: row.genres,
|
||||||
tags: row.tags,
|
tags: row.tags,
|
||||||
narrators: row.narrators,
|
narrators: row.narrators,
|
||||||
@@ -1458,6 +1465,7 @@ export class BookService {
|
|||||||
if (dto.goodreadsId !== undefined) scalarFields.goodreadsId = dto.goodreadsId ?? null;
|
if (dto.goodreadsId !== undefined) scalarFields.goodreadsId = dto.goodreadsId ?? null;
|
||||||
if (dto.amazonId !== undefined) scalarFields.amazonId = dto.amazonId ?? null;
|
if (dto.amazonId !== undefined) scalarFields.amazonId = dto.amazonId ?? null;
|
||||||
if (dto.hardcoverId !== undefined) scalarFields.hardcoverId = dto.hardcoverId ?? null;
|
if (dto.hardcoverId !== undefined) scalarFields.hardcoverId = dto.hardcoverId ?? null;
|
||||||
|
if (dto.hardcoverEditionId !== undefined) scalarFields.hardcoverEditionId = dto.hardcoverEditionId ?? null;
|
||||||
if (dto.openLibraryId !== undefined) scalarFields.openLibraryId = dto.openLibraryId ?? null;
|
if (dto.openLibraryId !== undefined) scalarFields.openLibraryId = dto.openLibraryId ?? null;
|
||||||
if (dto.itunesId !== undefined) scalarFields.itunesId = dto.itunesId ?? null;
|
if (dto.itunesId !== undefined) scalarFields.itunesId = dto.itunesId ?? null;
|
||||||
if (dto.audibleId !== undefined) scalarFields.audibleId = dto.audibleId ?? null;
|
if (dto.audibleId !== undefined) scalarFields.audibleId = dto.audibleId ?? null;
|
||||||
@@ -2704,6 +2712,7 @@ export class BookService {
|
|||||||
seriesMemberships: seriesMembershipRows,
|
seriesMemberships: seriesMembershipRows,
|
||||||
rating: personalRating,
|
rating: personalRating,
|
||||||
coverSource: (meta?.coverSource as 'extracted' | 'custom' | null) ?? null,
|
coverSource: (meta?.coverSource as 'extracted' | 'custom' | null) ?? null,
|
||||||
|
hardcoverEditionId: meta?.hardcoverEditionId ?? null,
|
||||||
lockedFields: this.bookMetadataLockService.normalizeLockedFields(meta?.lockedFields),
|
lockedFields: this.bookMetadataLockService.normalizeLockedFields(meta?.lockedFields),
|
||||||
providerIds: {
|
providerIds: {
|
||||||
[MetadataProviderKey.GOOGLE]: meta?.googleBooksId ?? null,
|
[MetadataProviderKey.GOOGLE]: meta?.googleBooksId ?? null,
|
||||||
@@ -2823,6 +2832,7 @@ export class BookService {
|
|||||||
goodreadsId: parsed.goodreadsId,
|
goodreadsId: parsed.goodreadsId,
|
||||||
amazonId: parsed.amazonId,
|
amazonId: parsed.amazonId,
|
||||||
hardcoverId: parsed.hardcoverId,
|
hardcoverId: parsed.hardcoverId,
|
||||||
|
hardcoverEditionId: parsed.hardcoverEditionId,
|
||||||
openLibraryId: parsed.openLibraryId,
|
openLibraryId: parsed.openLibraryId,
|
||||||
itunesId: parsed.itunesId,
|
itunesId: parsed.itunesId,
|
||||||
koboId: parsed.koboId,
|
koboId: parsed.koboId,
|
||||||
@@ -2856,6 +2866,7 @@ export class BookService {
|
|||||||
goodreadsId: parsed.goodreadsId,
|
goodreadsId: parsed.goodreadsId,
|
||||||
amazonId: parsed.amazonId,
|
amazonId: parsed.amazonId,
|
||||||
hardcoverId: parsed.hardcoverId,
|
hardcoverId: parsed.hardcoverId,
|
||||||
|
hardcoverEditionId: parsed.hardcoverEditionId,
|
||||||
openLibraryId: parsed.openLibraryId,
|
openLibraryId: parsed.openLibraryId,
|
||||||
itunesId: parsed.itunesId,
|
itunesId: parsed.itunesId,
|
||||||
koboId: parsed.koboId,
|
koboId: parsed.koboId,
|
||||||
@@ -2906,6 +2917,7 @@ export class BookService {
|
|||||||
goodreadsId: parsed.goodreadsId,
|
goodreadsId: parsed.goodreadsId,
|
||||||
amazonId: parsed.amazonId,
|
amazonId: parsed.amazonId,
|
||||||
hardcoverId: parsed.hardcoverId,
|
hardcoverId: parsed.hardcoverId,
|
||||||
|
hardcoverEditionId: parsed.hardcoverEditionId,
|
||||||
openLibraryId: parsed.openLibraryId,
|
openLibraryId: parsed.openLibraryId,
|
||||||
itunesId: parsed.itunesId,
|
itunesId: parsed.itunesId,
|
||||||
koboId: parsed.koboId,
|
koboId: parsed.koboId,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export class BookDetailDto {
|
|||||||
seriesMemberships: BookSeriesMembership[];
|
seriesMemberships: BookSeriesMembership[];
|
||||||
rating: number | null;
|
rating: number | null;
|
||||||
coverSource: 'extracted' | 'custom' | null;
|
coverSource: 'extracted' | 'custom' | null;
|
||||||
|
hardcoverEditionId: string | null;
|
||||||
providerIds: ProviderIds;
|
providerIds: ProviderIds;
|
||||||
authors: { id: number; name: string; sortName: string | null }[];
|
authors: { id: number; name: string; sortName: string | null }[];
|
||||||
genres: string[];
|
genres: string[];
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export class UpdateBookMetadataDto {
|
|||||||
@IsOptional() @IsString() @MaxLength(50) goodreadsId?: string | null;
|
@IsOptional() @IsString() @MaxLength(50) goodreadsId?: string | null;
|
||||||
@IsOptional() @IsString() @MaxLength(20) amazonId?: string | null;
|
@IsOptional() @IsString() @MaxLength(20) amazonId?: string | null;
|
||||||
@IsOptional() @IsString() @MaxLength(255) hardcoverId?: string | null;
|
@IsOptional() @IsString() @MaxLength(255) hardcoverId?: string | null;
|
||||||
|
@IsOptional() @IsString() @MaxLength(50) hardcoverEditionId?: string | null;
|
||||||
@IsOptional() @IsString() @MaxLength(50) openLibraryId?: string | null;
|
@IsOptional() @IsString() @MaxLength(50) openLibraryId?: string | null;
|
||||||
@IsOptional() @IsString() @MaxLength(50) itunesId?: string | null;
|
@IsOptional() @IsString() @MaxLength(50) itunesId?: string | null;
|
||||||
@IsOptional() @IsString() @MaxLength(20) audibleId?: string | null;
|
@IsOptional() @IsString() @MaxLength(20) audibleId?: string | null;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ function makeBookRow(id: number, overrides?: Partial<Parameters<typeof assembleB
|
|||||||
publisher: null,
|
publisher: null,
|
||||||
pageCount: null,
|
pageCount: null,
|
||||||
isbn13: null,
|
isbn13: null,
|
||||||
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -47,6 +49,15 @@ describe('assembleBookCards', () => {
|
|||||||
expect(card.files).toEqual([{ id: 10, format: 'epub', role: 'primary', sizeBytes: 4096 }]);
|
expect(card.files).toEqual([{ id: 10, format: 'epub', role: 'primary', sizeBytes: 4096 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes through Hardcover book and edition identifiers', () => {
|
||||||
|
const rows = [makeBookRow(1, { hardcoverId: 'new-orleans-rush', hardcoverEditionId: '8941973' })];
|
||||||
|
|
||||||
|
const [card] = assembleBookCards(rows, [], [], [], []);
|
||||||
|
|
||||||
|
expect(card.hardcoverId).toBe('new-orleans-rush');
|
||||||
|
expect(card.hardcoverEditionId).toBe('8941973');
|
||||||
|
});
|
||||||
|
|
||||||
it('falls back to basename of folderPath when title is null', () => {
|
it('falls back to basename of folderPath when title is null', () => {
|
||||||
const rows = [makeBookRow(2, { title: null, folderPath: '/books/my-book-folder' })];
|
const rows = [makeBookRow(2, { title: null, folderPath: '/books/my-book-folder' })];
|
||||||
|
|
||||||
@@ -351,6 +362,8 @@ function makeBookCard(id: number, overrides?: Record<string, unknown>) {
|
|||||||
publisher: null as string | null,
|
publisher: null as string | null,
|
||||||
pageCount: null as number | null,
|
pageCount: null as number | null,
|
||||||
isbn13: null as string | null,
|
isbn13: null as string | null,
|
||||||
|
hardcoverId: null as string | null,
|
||||||
|
hardcoverEditionId: null as string | null,
|
||||||
narrators: [] as string[],
|
narrators: [] as string[],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ type BookRow = {
|
|||||||
publisher: string | null;
|
publisher: string | null;
|
||||||
pageCount: number | null;
|
pageCount: number | null;
|
||||||
isbn13: string | null;
|
isbn13: string | null;
|
||||||
|
hardcoverId: string | null;
|
||||||
|
hardcoverEditionId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CollapsedBookRow = BookRow & {
|
type CollapsedBookRow = BookRow & {
|
||||||
@@ -185,6 +187,8 @@ export function assembleBookCards(
|
|||||||
publisher: row.publisher ?? null,
|
publisher: row.publisher ?? null,
|
||||||
pageCount: row.pageCount ?? null,
|
pageCount: row.pageCount ?? null,
|
||||||
isbn13: row.isbn13 ?? null,
|
isbn13: row.isbn13 ?? null,
|
||||||
|
hardcoverId: row.hardcoverId ?? null,
|
||||||
|
hardcoverEditionId: row.hardcoverEditionId ?? null,
|
||||||
narrators: narratorsByBook.get(row.id) ?? [],
|
narrators: narratorsByBook.get(row.id) ?? [],
|
||||||
tags: tagsByBook.get(row.id) ?? [],
|
tags: tagsByBook.get(row.id) ?? [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const COMIC_INFO_MANAGED_NOTES_KEYS = [
|
|||||||
'goodreadsId',
|
'goodreadsId',
|
||||||
'amazonId',
|
'amazonId',
|
||||||
'hardcoverId',
|
'hardcoverId',
|
||||||
|
'hardcoverEditionId',
|
||||||
'googleBooksId',
|
'googleBooksId',
|
||||||
'openLibraryId',
|
'openLibraryId',
|
||||||
'ranobedbId',
|
'ranobedbId',
|
||||||
@@ -57,6 +58,7 @@ export const EPUB_PROVIDER_IDENTIFIER_SCHEMES = {
|
|||||||
goodreadsId: 'GOODREADS',
|
goodreadsId: 'GOODREADS',
|
||||||
amazonId: 'AMAZON',
|
amazonId: 'AMAZON',
|
||||||
hardcoverId: 'HARDCOVER',
|
hardcoverId: 'HARDCOVER',
|
||||||
|
hardcoverEditionId: 'HARDCOVER_EDITION',
|
||||||
googleBooksId: 'GOOGLE',
|
googleBooksId: 'GOOGLE',
|
||||||
openLibraryId: 'OPENLIBRARY',
|
openLibraryId: 'OPENLIBRARY',
|
||||||
ranobedbId: 'RANOBEDB',
|
ranobedbId: 'RANOBEDB',
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ describe('FileWriteRepository', () => {
|
|||||||
goodreadsId: 'gr',
|
goodreadsId: 'gr',
|
||||||
amazonId: 'a',
|
amazonId: 'a',
|
||||||
hardcoverId: 'h',
|
hardcoverId: 'h',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
openLibraryId: 'ol',
|
openLibraryId: 'ol',
|
||||||
ranobedbId: 'rn',
|
ranobedbId: 'rn',
|
||||||
koboId: 'kb',
|
koboId: 'kb',
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ export class FileWriteRepository {
|
|||||||
goodreadsId: meta.goodreadsId,
|
goodreadsId: meta.goodreadsId,
|
||||||
amazonId: meta.amazonId,
|
amazonId: meta.amazonId,
|
||||||
hardcoverId: meta.hardcoverId,
|
hardcoverId: meta.hardcoverId,
|
||||||
|
hardcoverEditionId: meta.hardcoverEditionId,
|
||||||
openLibraryId: meta.openLibraryId,
|
openLibraryId: meta.openLibraryId,
|
||||||
ranobedbId: meta.ranobedbId,
|
ranobedbId: meta.ranobedbId,
|
||||||
koboId: meta.koboId,
|
koboId: meta.koboId,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ describe('buildComicInfoXml', () => {
|
|||||||
rating: 8,
|
rating: 8,
|
||||||
isbn13: '9780441172719',
|
isbn13: '9780441172719',
|
||||||
goodreadsId: '44767458-dune',
|
goodreadsId: '44767458-dune',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
ranobedbId: 'ranobe-1',
|
ranobedbId: 'ranobe-1',
|
||||||
subtitle: 'Anniversary Edition',
|
subtitle: 'Anniversary Edition',
|
||||||
isbn10: '0441172717',
|
isbn10: '0441172717',
|
||||||
@@ -47,6 +48,7 @@ describe('buildComicInfoXml', () => {
|
|||||||
'rating',
|
'rating',
|
||||||
'isbn13',
|
'isbn13',
|
||||||
'goodreadsId',
|
'goodreadsId',
|
||||||
|
'hardcoverEditionId',
|
||||||
'ranobedbId',
|
'ranobedbId',
|
||||||
'subtitle',
|
'subtitle',
|
||||||
'isbn10',
|
'isbn10',
|
||||||
@@ -64,6 +66,7 @@ describe('buildComicInfoXml', () => {
|
|||||||
expect(parsed.ComicInfo.Web).toBe('https://www.goodreads.com/book/show/44767458-dune');
|
expect(parsed.ComicInfo.Web).toBe('https://www.goodreads.com/book/show/44767458-dune');
|
||||||
expect(parsed.ComicInfo.Notes).toContain('[bookorbit:subtitle] Anniversary Edition');
|
expect(parsed.ComicInfo.Notes).toContain('[bookorbit:subtitle] Anniversary Edition');
|
||||||
expect(parsed.ComicInfo.Notes).toContain('[bookorbit:isbn10] 0441172717');
|
expect(parsed.ComicInfo.Notes).toContain('[bookorbit:isbn10] 0441172717');
|
||||||
|
expect(parsed.ComicInfo.Notes).toContain('[bookorbit:hardcoverEditionId] 8941973');
|
||||||
expect(parsed.ComicInfo.Notes).toContain('[bookorbit:ranobedbId] ranobe-1');
|
expect(parsed.ComicInfo.Notes).toContain('[bookorbit:ranobedbId] ranobe-1');
|
||||||
expect(parsed.ComicInfo['@_xmlns:xsi']).toBeDefined();
|
expect(parsed.ComicInfo['@_xmlns:xsi']).toBeDefined();
|
||||||
expect(parsed.ComicInfo['@_xmlns:xsd']).toBeDefined();
|
expect(parsed.ComicInfo['@_xmlns:xsd']).toBeDefined();
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ describe('epub-opf-builder', () => {
|
|||||||
goodreadsId: '42129393',
|
goodreadsId: '42129393',
|
||||||
openLibraryId: 'OL20652610W',
|
openLibraryId: 'OL20652610W',
|
||||||
hardcoverId: 'new-orleans-rush',
|
hardcoverId: 'new-orleans-rush',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
ranobedbId: 'ranobe-1',
|
ranobedbId: 'ranobe-1',
|
||||||
itunesId: '123456789',
|
itunesId: '123456789',
|
||||||
});
|
});
|
||||||
@@ -88,6 +89,8 @@ describe('epub-opf-builder', () => {
|
|||||||
expect(result.newOpfXml).toContain('opf:scheme="GOODREADS"');
|
expect(result.newOpfXml).toContain('opf:scheme="GOODREADS"');
|
||||||
expect(result.newOpfXml).toContain('opf:scheme="OPENLIBRARY"');
|
expect(result.newOpfXml).toContain('opf:scheme="OPENLIBRARY"');
|
||||||
expect(result.newOpfXml).toContain('opf:scheme="HARDCOVER"');
|
expect(result.newOpfXml).toContain('opf:scheme="HARDCOVER"');
|
||||||
|
expect(result.newOpfXml).toContain('opf:scheme="HARDCOVER_EDITION"');
|
||||||
|
expect(result.newOpfXml).toContain('>8941973<');
|
||||||
expect(result.newOpfXml).toContain('opf:scheme="RANOBEDB"');
|
expect(result.newOpfXml).toContain('opf:scheme="RANOBEDB"');
|
||||||
expect(result.newOpfXml).toContain('>ranobe-1<');
|
expect(result.newOpfXml).toContain('>ranobe-1<');
|
||||||
expect(result.newOpfXml).toContain('opf:scheme="ITUNES"');
|
expect(result.newOpfXml).toContain('opf:scheme="ITUNES"');
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ describe('buildXmp', () => {
|
|||||||
googleBooksId: 'google-1',
|
googleBooksId: 'google-1',
|
||||||
amazonId: 'amazon-1',
|
amazonId: 'amazon-1',
|
||||||
hardcoverId: 'hardcover-1',
|
hardcoverId: 'hardcover-1',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
openLibraryId: 'OL123W',
|
openLibraryId: 'OL123W',
|
||||||
itunesId: 'itunes-1',
|
itunesId: 'itunes-1',
|
||||||
tags: ['classic'],
|
tags: ['classic'],
|
||||||
@@ -99,6 +100,7 @@ describe('buildXmp', () => {
|
|||||||
'googleBooksId',
|
'googleBooksId',
|
||||||
'amazonId',
|
'amazonId',
|
||||||
'hardcoverId',
|
'hardcoverId',
|
||||||
|
'hardcoverEditionId',
|
||||||
'openLibraryId',
|
'openLibraryId',
|
||||||
'itunesId',
|
'itunesId',
|
||||||
'tags',
|
'tags',
|
||||||
@@ -110,6 +112,7 @@ describe('buildXmp', () => {
|
|||||||
expect(xmp).toContain('<dc:publisher>Ace</dc:publisher>');
|
expect(xmp).toContain('<dc:publisher>Ace</dc:publisher>');
|
||||||
expect(xmp).toContain('<bookorbit:isbn13>9780441172719</bookorbit:isbn13>');
|
expect(xmp).toContain('<bookorbit:isbn13>9780441172719</bookorbit:isbn13>');
|
||||||
expect(xmp).toContain('<bookorbit:googleBooksId>google-1</bookorbit:googleBooksId>');
|
expect(xmp).toContain('<bookorbit:googleBooksId>google-1</bookorbit:googleBooksId>');
|
||||||
|
expect(xmp).toContain('<bookorbit:hardcoverEditionId>8941973</bookorbit:hardcoverEditionId>');
|
||||||
expect(xmp).toContain('<bookorbit:itunesId>itunes-1</bookorbit:itunesId>');
|
expect(xmp).toContain('<bookorbit:itunesId>itunes-1</bookorbit:itunesId>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface BookWritePayload {
|
|||||||
goodreadsId?: string | null;
|
goodreadsId?: string | null;
|
||||||
amazonId?: string | null;
|
amazonId?: string | null;
|
||||||
hardcoverId?: string | null;
|
hardcoverId?: string | null;
|
||||||
|
hardcoverEditionId?: string | null;
|
||||||
openLibraryId?: string | null;
|
openLibraryId?: string | null;
|
||||||
ranobedbId?: string | null;
|
ranobedbId?: string | null;
|
||||||
koboId?: string | null;
|
koboId?: string | null;
|
||||||
|
|||||||
@@ -486,6 +486,33 @@ describe('MetadataFetchPipeline', () => {
|
|||||||
expect(providerIds).toEqual({ [MetadataProviderKey.GOOGLE]: 'g1' });
|
expect(providerIds).toEqual({ [MetadataProviderKey.GOOGLE]: 'g1' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes through Hardcover edition id with provider ids when saveProviderIds is enabled', async () => {
|
||||||
|
const prefs = createPreferences((fields) => {
|
||||||
|
fields.title = {
|
||||||
|
enabled: true,
|
||||||
|
providers: [MetadataProviderKey.HARDCOVER],
|
||||||
|
mergeStrategy: 'overwriteIfProvided',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
prefs.options = {
|
||||||
|
genres: { mode: 'firstProvider', blocklist: [] },
|
||||||
|
saveProviderIds: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
preferencesService.getGlobal.mockResolvedValue(prefs);
|
||||||
|
resolver.resolve.mockReturnValue(prefs);
|
||||||
|
resolver.withForwardCompatibility.mockReturnValue(prefs);
|
||||||
|
registry.all.mockReturnValue([{ key: MetadataProviderKey.HARDCOVER }] as never);
|
||||||
|
fetchService.search.mockReturnValue(
|
||||||
|
of(candidate(MetadataProviderKey.HARDCOVER, 'the-name-of-the-wind', { title: 'Fetched Title', hardcoverEditionId: '1001' })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { resolved, providerIds } = await pipeline.runWithSources({ title: 'Query' }, {});
|
||||||
|
|
||||||
|
expect(resolved.hardcoverEditionId).toBe('1001');
|
||||||
|
expect(providerIds).toEqual({ [MetadataProviderKey.HARDCOVER]: 'the-name-of-the-wind' });
|
||||||
|
});
|
||||||
|
|
||||||
it('does not return provider ids when saveProviderIds is disabled', async () => {
|
it('does not return provider ids when saveProviderIds is disabled', async () => {
|
||||||
const prefs = createPreferences((fields) => {
|
const prefs = createPreferences((fields) => {
|
||||||
fields.title = {
|
fields.title = {
|
||||||
@@ -510,6 +537,33 @@ describe('MetadataFetchPipeline', () => {
|
|||||||
expect(providerIds).toEqual({});
|
expect(providerIds).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not pass through Hardcover edition id when provider id saving is disabled', async () => {
|
||||||
|
const prefs = createPreferences((fields) => {
|
||||||
|
fields.title = {
|
||||||
|
enabled: true,
|
||||||
|
providers: [MetadataProviderKey.HARDCOVER],
|
||||||
|
mergeStrategy: 'overwriteIfProvided',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
prefs.options = {
|
||||||
|
genres: { mode: 'firstProvider', blocklist: [] },
|
||||||
|
saveProviderIds: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
preferencesService.getGlobal.mockResolvedValue(prefs);
|
||||||
|
resolver.resolve.mockReturnValue(prefs);
|
||||||
|
resolver.withForwardCompatibility.mockReturnValue(prefs);
|
||||||
|
registry.all.mockReturnValue([{ key: MetadataProviderKey.HARDCOVER }] as never);
|
||||||
|
fetchService.search.mockReturnValue(
|
||||||
|
of(candidate(MetadataProviderKey.HARDCOVER, 'the-name-of-the-wind', { title: 'Fetched Title', hardcoverEditionId: '1001' })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { resolved, providerIds } = await pipeline.runWithSources({ title: 'Query' }, {});
|
||||||
|
|
||||||
|
expect(resolved.hardcoverEditionId).toBeUndefined();
|
||||||
|
expect(providerIds).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps the first candidate from each provider to avoid provider stream reordering issues', async () => {
|
it('keeps the first candidate from each provider to avoid provider stream reordering issues', async () => {
|
||||||
const prefs = createPreferences((fields) => {
|
const prefs = createPreferences((fields) => {
|
||||||
fields.description = {
|
fields.description = {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { MetadataSearchParams } from './providers/metadata-search-params';
|
|||||||
|
|
||||||
export type ResolvedMetadataFields = Partial<Record<MetadataField, string | string[] | number | null>> & {
|
export type ResolvedMetadataFields = Partial<Record<MetadataField, string | string[] | number | null>> & {
|
||||||
coverUrl?: string;
|
coverUrl?: string;
|
||||||
|
hardcoverEditionId?: string | null;
|
||||||
seriesMemberships?: MetadataSeriesMembership[];
|
seriesMemberships?: MetadataSeriesMembership[];
|
||||||
chapters?: AudiobookChapter[];
|
chapters?: AudiobookChapter[];
|
||||||
comicMetadata?: ComicMetadataFields;
|
comicMetadata?: ComicMetadataFields;
|
||||||
@@ -295,6 +296,9 @@ export class MetadataFetchPipeline {
|
|||||||
if (preferences.options?.saveProviderIds) {
|
if (preferences.options?.saveProviderIds) {
|
||||||
for (const candidate of byProvider.values()) {
|
for (const candidate of byProvider.values()) {
|
||||||
if (candidate.providerId) providerIds[candidate.provider] = candidate.providerId;
|
if (candidate.providerId) providerIds[candidate.provider] = candidate.providerId;
|
||||||
|
if (candidate.provider === MetadataProviderKey.HARDCOVER && candidate.hardcoverEditionId) {
|
||||||
|
result.hardcoverEditionId = candidate.hardcoverEditionId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ describe('mapSearchDocument', () => {
|
|||||||
coverUrl: 'https://assets.hardcover.app/cover.jpg',
|
coverUrl: 'https://assets.hardcover.app/cover.jpg',
|
||||||
sourceUrl: 'https://hardcover.app/books/the-name-of-the-wind',
|
sourceUrl: 'https://hardcover.app/books/the-name-of-the-wind',
|
||||||
});
|
});
|
||||||
|
expect(result.hardcoverEditionId).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prefers release_year over release_date for publishedYear', () => {
|
it('prefers release_year over release_date for publishedYear', () => {
|
||||||
@@ -144,6 +145,7 @@ describe('mapBookWithEditions', () => {
|
|||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
provider: MetadataProviderKey.HARDCOVER,
|
provider: MetadataProviderKey.HARDCOVER,
|
||||||
providerId: 'the-name-of-the-wind',
|
providerId: 'the-name-of-the-wind',
|
||||||
|
hardcoverEditionId: '1001',
|
||||||
title: 'The Name of the Wind',
|
title: 'The Name of the Wind',
|
||||||
subtitle: 'Edition Subtitle',
|
subtitle: 'Edition Subtitle',
|
||||||
description: 'A story about a wizard.',
|
description: 'A story about a wizard.',
|
||||||
@@ -182,7 +184,9 @@ describe('mapBookWithEditions', () => {
|
|||||||
const results = mapBookWithEditions(book);
|
const results = mapBookWithEditions(book);
|
||||||
expect(results).toHaveLength(2);
|
expect(results).toHaveLength(2);
|
||||||
expect(results[0].isbn13).toBe('9780000000001');
|
expect(results[0].isbn13).toBe('9780000000001');
|
||||||
|
expect(results[0].hardcoverEditionId).toBe('1001');
|
||||||
expect(results[1].isbn13).toBe('9780000000002');
|
expect(results[1].isbn13).toBe('9780000000002');
|
||||||
|
expect(results[1].hardcoverEditionId).toBe('1002');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses edition authors when present', () => {
|
it('uses edition authors when present', () => {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ function mapEdition(edition: HardcoverEdition, book: HardcoverBookWithEditions):
|
|||||||
return {
|
return {
|
||||||
provider: MetadataProviderKey.HARDCOVER,
|
provider: MetadataProviderKey.HARDCOVER,
|
||||||
providerId: book.slug,
|
providerId: book.slug,
|
||||||
|
hardcoverEditionId: String(edition.id),
|
||||||
title: edition.title ?? book.title,
|
title: edition.title ?? book.title,
|
||||||
subtitle: edition.subtitle ?? book.subtitle,
|
subtitle: edition.subtitle ?? book.subtitle,
|
||||||
description: book.description,
|
description: book.description,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export class ComicFormatExtractor implements FormatExtractor {
|
|||||||
goodreadsId: comicMetadata?.goodreadsId ?? null,
|
goodreadsId: comicMetadata?.goodreadsId ?? null,
|
||||||
amazonId: comicMetadata?.amazonId ?? null,
|
amazonId: comicMetadata?.amazonId ?? null,
|
||||||
hardcoverId: comicMetadata?.hardcoverId ?? null,
|
hardcoverId: comicMetadata?.hardcoverId ?? null,
|
||||||
|
hardcoverEditionId: comicMetadata?.hardcoverEditionId ?? null,
|
||||||
openLibraryId: comicMetadata?.openLibraryId ?? null,
|
openLibraryId: comicMetadata?.openLibraryId ?? null,
|
||||||
ranobedbId: comicMetadata?.ranobedbId ?? null,
|
ranobedbId: comicMetadata?.ranobedbId ?? null,
|
||||||
koboId: comicMetadata?.koboId ?? null,
|
koboId: comicMetadata?.koboId ?? null,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface ParsedBookData {
|
|||||||
goodreadsId?: string | null;
|
goodreadsId?: string | null;
|
||||||
amazonId?: string | null;
|
amazonId?: string | null;
|
||||||
hardcoverId?: string | null;
|
hardcoverId?: string | null;
|
||||||
|
hardcoverEditionId?: string | null;
|
||||||
openLibraryId?: string | null;
|
openLibraryId?: string | null;
|
||||||
ranobedbId?: string | null;
|
ranobedbId?: string | null;
|
||||||
koboId?: string | null;
|
koboId?: string | null;
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ describe('metadata format extractors', () => {
|
|||||||
goodreadsId: '234248',
|
goodreadsId: '234248',
|
||||||
amazonId: 'B00B7NPRY8',
|
amazonId: 'B00B7NPRY8',
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
openLibraryId: 'OL7353617M',
|
openLibraryId: 'OL7353617M',
|
||||||
ranobedbId: 'ranobe-1',
|
ranobedbId: 'ranobe-1',
|
||||||
itunesId: null,
|
itunesId: null,
|
||||||
@@ -147,6 +148,7 @@ describe('metadata format extractors', () => {
|
|||||||
googleBooksId: 'abc123',
|
googleBooksId: 'abc123',
|
||||||
goodreadsId: '234248',
|
goodreadsId: '234248',
|
||||||
amazonId: 'B00B7NPRY8',
|
amazonId: 'B00B7NPRY8',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
openLibraryId: 'OL7353617M',
|
openLibraryId: 'OL7353617M',
|
||||||
ranobedbId: 'ranobe-1',
|
ranobedbId: 'ranobe-1',
|
||||||
}),
|
}),
|
||||||
@@ -174,6 +176,7 @@ describe('metadata format extractors', () => {
|
|||||||
goodreadsId: null,
|
goodreadsId: null,
|
||||||
amazonId: null,
|
amazonId: null,
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
openLibraryId: null,
|
openLibraryId: null,
|
||||||
ranobedbId: null,
|
ranobedbId: null,
|
||||||
itunesId: null,
|
itunesId: null,
|
||||||
@@ -415,6 +418,7 @@ describe('metadata format extractors', () => {
|
|||||||
goodreadsId: null,
|
goodreadsId: null,
|
||||||
amazonId: null,
|
amazonId: null,
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
openLibraryId: null,
|
openLibraryId: null,
|
||||||
ranobedbId: null,
|
ranobedbId: null,
|
||||||
itunesId: null,
|
itunesId: null,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function hasOpfMetadata(metadata: ParsedOpf): boolean {
|
|||||||
metadata.goodreadsId !== null ||
|
metadata.goodreadsId !== null ||
|
||||||
metadata.amazonId !== null ||
|
metadata.amazonId !== null ||
|
||||||
metadata.hardcoverId !== null ||
|
metadata.hardcoverId !== null ||
|
||||||
|
metadata.hardcoverEditionId !== null ||
|
||||||
metadata.openLibraryId !== null ||
|
metadata.openLibraryId !== null ||
|
||||||
metadata.ranobedbId !== null ||
|
metadata.ranobedbId !== null ||
|
||||||
metadata.koboId !== null ||
|
metadata.koboId !== null ||
|
||||||
@@ -52,6 +53,7 @@ export function mapOpfMetadata(metadata: ParsedOpf, cover: Buffer | null): Parse
|
|||||||
goodreadsId: metadata.goodreadsId,
|
goodreadsId: metadata.goodreadsId,
|
||||||
amazonId: metadata.amazonId,
|
amazonId: metadata.amazonId,
|
||||||
hardcoverId: metadata.hardcoverId,
|
hardcoverId: metadata.hardcoverId,
|
||||||
|
hardcoverEditionId: metadata.hardcoverEditionId,
|
||||||
openLibraryId: metadata.openLibraryId,
|
openLibraryId: metadata.openLibraryId,
|
||||||
ranobedbId: metadata.ranobedbId,
|
ranobedbId: metadata.ranobedbId,
|
||||||
koboId: metadata.koboId,
|
koboId: metadata.koboId,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class PdfFormatExtractor implements FormatExtractor {
|
|||||||
goodreadsId: pdf.goodreadsId,
|
goodreadsId: pdf.goodreadsId,
|
||||||
amazonId: pdf.amazonId,
|
amazonId: pdf.amazonId,
|
||||||
hardcoverId: pdf.hardcoverId,
|
hardcoverId: pdf.hardcoverId,
|
||||||
|
hardcoverEditionId: pdf.hardcoverEditionId,
|
||||||
openLibraryId: pdf.openLibraryId,
|
openLibraryId: pdf.openLibraryId,
|
||||||
ranobedbId: pdf.ranobedbId,
|
ranobedbId: pdf.ranobedbId,
|
||||||
koboId: pdf.koboId,
|
koboId: pdf.koboId,
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ describe('extractCbzMetadata', () => {
|
|||||||
Manual note
|
Manual note
|
||||||
[bookorbit:ranobedbId] ranobe-1
|
[bookorbit:ranobedbId] ranobe-1
|
||||||
[bookorbit:googleBooksId] google-1
|
[bookorbit:googleBooksId] google-1
|
||||||
|
[bookorbit:hardcoverEditionId] 8941973
|
||||||
</Notes>
|
</Notes>
|
||||||
</ComicInfo>`;
|
</ComicInfo>`;
|
||||||
mockReadFile.mockResolvedValue(buildZipWithComicInfo(xml) as unknown as Buffer);
|
mockReadFile.mockResolvedValue(buildZipWithComicInfo(xml) as unknown as Buffer);
|
||||||
@@ -206,6 +207,7 @@ describe('extractCbzMetadata', () => {
|
|||||||
const r = await extractCbzMetadata('/book.cbz');
|
const r = await extractCbzMetadata('/book.cbz');
|
||||||
expect(r?.ranobedbId).toBe('ranobe-1');
|
expect(r?.ranobedbId).toBe('ranobe-1');
|
||||||
expect(r?.googleBooksId).toBe('google-1');
|
expect(r?.googleBooksId).toBe('google-1');
|
||||||
|
expect(r?.hardcoverEditionId).toBe('8941973');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface ParsedCbzMetadata {
|
|||||||
goodreadsId: string | null;
|
goodreadsId: string | null;
|
||||||
amazonId: string | null;
|
amazonId: string | null;
|
||||||
hardcoverId: string | null;
|
hardcoverId: string | null;
|
||||||
|
hardcoverEditionId: string | null;
|
||||||
openLibraryId: string | null;
|
openLibraryId: string | null;
|
||||||
ranobedbId: string | null;
|
ranobedbId: string | null;
|
||||||
koboId: string | null;
|
koboId: string | null;
|
||||||
@@ -266,6 +267,7 @@ function parseComicInfoXml(xmlBuf: Buffer): ParsedCbzMetadata | null {
|
|||||||
goodreadsId: managedNotes.get('goodreadsId') ?? providerIdsFromWeb.goodreadsId ?? null,
|
goodreadsId: managedNotes.get('goodreadsId') ?? providerIdsFromWeb.goodreadsId ?? null,
|
||||||
amazonId: managedNotes.get('amazonId') ?? providerIdsFromWeb.amazonId ?? null,
|
amazonId: managedNotes.get('amazonId') ?? providerIdsFromWeb.amazonId ?? null,
|
||||||
hardcoverId: managedNotes.get('hardcoverId') ?? providerIdsFromWeb.hardcoverId ?? null,
|
hardcoverId: managedNotes.get('hardcoverId') ?? providerIdsFromWeb.hardcoverId ?? null,
|
||||||
|
hardcoverEditionId: managedNotes.get('hardcoverEditionId') ?? null,
|
||||||
openLibraryId: managedNotes.get('openLibraryId') ?? providerIdsFromWeb.openLibraryId ?? null,
|
openLibraryId: managedNotes.get('openLibraryId') ?? providerIdsFromWeb.openLibraryId ?? null,
|
||||||
ranobedbId: managedNotes.get('ranobedbId') ?? null,
|
ranobedbId: managedNotes.get('ranobedbId') ?? null,
|
||||||
koboId: managedNotes.get('koboId') ?? providerIdsFromWeb.koboId ?? null,
|
koboId: managedNotes.get('koboId') ?? providerIdsFromWeb.koboId ?? null,
|
||||||
@@ -325,6 +327,7 @@ function parseComicBookInfoJson(comment: string): ParsedCbzMetadata | null {
|
|||||||
goodreadsId: null,
|
goodreadsId: null,
|
||||||
amazonId: null,
|
amazonId: null,
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
openLibraryId: null,
|
openLibraryId: null,
|
||||||
ranobedbId: null,
|
ranobedbId: null,
|
||||||
koboId: null,
|
koboId: null,
|
||||||
|
|||||||
@@ -384,6 +384,12 @@ describe('parseOpf', () => {
|
|||||||
expect(r.hardcoverId).toBe('new-orleans-rush');
|
expect(r.hardcoverId).toBe('new-orleans-rush');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses Hardcover edition ID from opf:scheme, urn, and prefix formats', () => {
|
||||||
|
expect(parseOpf(epub2Opf(`<dc:identifier opf:scheme="HARDCOVER_EDITION">8941973</dc:identifier>`)).hardcoverEditionId).toBe('8941973');
|
||||||
|
expect(parseOpf(epub2Opf(`<dc:identifier>urn:hardcover_edition:8941974</dc:identifier>`)).hardcoverEditionId).toBe('8941974');
|
||||||
|
expect(parseOpf(epub3Opf(`<dc:identifier>hardcover-edition:8941975</dc:identifier>`)).hardcoverEditionId).toBe('8941975');
|
||||||
|
});
|
||||||
|
|
||||||
it('parses iTunes ID from opf:scheme attribute', () => {
|
it('parses iTunes ID from opf:scheme attribute', () => {
|
||||||
const xml = epub2Opf(`<dc:identifier opf:scheme="ITUNES">123456789</dc:identifier>`);
|
const xml = epub2Opf(`<dc:identifier opf:scheme="ITUNES">123456789</dc:identifier>`);
|
||||||
const r = parseOpf(xml);
|
const r = parseOpf(xml);
|
||||||
@@ -461,6 +467,7 @@ describe('parseOpf', () => {
|
|||||||
expect(r.amazonId).toBeNull();
|
expect(r.amazonId).toBeNull();
|
||||||
expect(r.goodreadsId).toBeNull();
|
expect(r.goodreadsId).toBeNull();
|
||||||
expect(r.hardcoverId).toBeNull();
|
expect(r.hardcoverId).toBeNull();
|
||||||
|
expect(r.hardcoverEditionId).toBeNull();
|
||||||
expect(r.openLibraryId).toBeNull();
|
expect(r.openLibraryId).toBeNull();
|
||||||
expect(r.ranobedbId).toBeNull();
|
expect(r.ranobedbId).toBeNull();
|
||||||
expect(r.itunesId).toBeNull();
|
expect(r.itunesId).toBeNull();
|
||||||
@@ -474,6 +481,7 @@ describe('parseOpf', () => {
|
|||||||
<dc:identifier>google:ABCD1234</dc:identifier>
|
<dc:identifier>google:ABCD1234</dc:identifier>
|
||||||
<dc:identifier>openlibrary:OL99999999W</dc:identifier>
|
<dc:identifier>openlibrary:OL99999999W</dc:identifier>
|
||||||
<dc:identifier>hardcover:test-book-slug</dc:identifier>
|
<dc:identifier>hardcover:test-book-slug</dc:identifier>
|
||||||
|
<dc:identifier>hardcover_edition:8941973</dc:identifier>
|
||||||
<dc:identifier>kobo:test-kobo-id</dc:identifier>
|
<dc:identifier>kobo:test-kobo-id</dc:identifier>
|
||||||
<dc:identifier>itunes:987654321</dc:identifier>
|
<dc:identifier>itunes:987654321</dc:identifier>
|
||||||
<dc:identifier>lubimyczytac:lub-99999</dc:identifier>
|
<dc:identifier>lubimyczytac:lub-99999</dc:identifier>
|
||||||
@@ -485,6 +493,7 @@ describe('parseOpf', () => {
|
|||||||
expect(r.googleBooksId).toBe('ABCD1234');
|
expect(r.googleBooksId).toBe('ABCD1234');
|
||||||
expect(r.openLibraryId).toBe('OL99999999W');
|
expect(r.openLibraryId).toBe('OL99999999W');
|
||||||
expect(r.hardcoverId).toBe('test-book-slug');
|
expect(r.hardcoverId).toBe('test-book-slug');
|
||||||
|
expect(r.hardcoverEditionId).toBe('8941973');
|
||||||
expect(r.koboId).toBe('test-kobo-id');
|
expect(r.koboId).toBe('test-kobo-id');
|
||||||
expect(r.itunesId).toBe('987654321');
|
expect(r.itunesId).toBe('987654321');
|
||||||
expect(r.lubimyczytacId).toBe('lub-99999');
|
expect(r.lubimyczytacId).toBe('lub-99999');
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface ParsedOpf {
|
|||||||
goodreadsId: string | null;
|
goodreadsId: string | null;
|
||||||
amazonId: string | null;
|
amazonId: string | null;
|
||||||
hardcoverId: string | null;
|
hardcoverId: string | null;
|
||||||
|
hardcoverEditionId: string | null;
|
||||||
openLibraryId: string | null;
|
openLibraryId: string | null;
|
||||||
ranobedbId: string | null;
|
ranobedbId: string | null;
|
||||||
koboId: string | null;
|
koboId: string | null;
|
||||||
@@ -88,7 +89,18 @@ function parseBookOrbitTags(raw: string | null): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderKey = 'google' | 'amazon' | 'goodreads' | 'hardcover' | 'openlibrary' | 'ranobedb' | 'kobo' | 'lubimyczytac' | 'aladin' | 'itunes';
|
type ProviderKey =
|
||||||
|
| 'google'
|
||||||
|
| 'amazon'
|
||||||
|
| 'goodreads'
|
||||||
|
| 'hardcover'
|
||||||
|
| 'hardcoverEdition'
|
||||||
|
| 'openlibrary'
|
||||||
|
| 'ranobedb'
|
||||||
|
| 'kobo'
|
||||||
|
| 'lubimyczytac'
|
||||||
|
| 'aladin'
|
||||||
|
| 'itunes';
|
||||||
|
|
||||||
// Calibre 9.x (opf3) writes provider identifiers as bare `prefix:value` text inside <dc:identifier>.
|
// Calibre 9.x (opf3) writes provider identifiers as bare `prefix:value` text inside <dc:identifier>.
|
||||||
// Only these known prefixes are recognized, as a lowest-priority fallback after opf:scheme and urn:.
|
// Only these known prefixes are recognized, as a lowest-priority fallback after opf:scheme and urn:.
|
||||||
@@ -100,6 +112,9 @@ const PREFIX_TO_PROVIDER: Record<string, ProviderKey> = {
|
|||||||
google: 'google',
|
google: 'google',
|
||||||
openlibrary: 'openlibrary',
|
openlibrary: 'openlibrary',
|
||||||
hardcover: 'hardcover',
|
hardcover: 'hardcover',
|
||||||
|
hardcover_edition: 'hardcoverEdition',
|
||||||
|
'hardcover-edition': 'hardcoverEdition',
|
||||||
|
hardcoveredition: 'hardcoverEdition',
|
||||||
kobo: 'kobo',
|
kobo: 'kobo',
|
||||||
itunes: 'itunes',
|
itunes: 'itunes',
|
||||||
lubimyczytac: 'lubimyczytac',
|
lubimyczytac: 'lubimyczytac',
|
||||||
@@ -261,6 +276,7 @@ export function parseOpf(xml: string): ParsedOpf {
|
|||||||
let schemeGoodreadsId: string | null = null;
|
let schemeGoodreadsId: string | null = null;
|
||||||
let schemeAmazonId: string | null = null;
|
let schemeAmazonId: string | null = null;
|
||||||
let schemeHardcoverId: string | null = null;
|
let schemeHardcoverId: string | null = null;
|
||||||
|
let schemeHardcoverEditionId: string | null = null;
|
||||||
let schemeOpenLibraryId: string | null = null;
|
let schemeOpenLibraryId: string | null = null;
|
||||||
let schemeRanobedbId: string | null = null;
|
let schemeRanobedbId: string | null = null;
|
||||||
let schemeKoboId: string | null = null;
|
let schemeKoboId: string | null = null;
|
||||||
@@ -272,6 +288,7 @@ export function parseOpf(xml: string): ParsedOpf {
|
|||||||
let urnGoodreadsId: string | null = null;
|
let urnGoodreadsId: string | null = null;
|
||||||
let urnAmazonId: string | null = null;
|
let urnAmazonId: string | null = null;
|
||||||
let urnHardcoverId: string | null = null;
|
let urnHardcoverId: string | null = null;
|
||||||
|
let urnHardcoverEditionId: string | null = null;
|
||||||
let urnOpenLibraryId: string | null = null;
|
let urnOpenLibraryId: string | null = null;
|
||||||
let urnRanobedbId: string | null = null;
|
let urnRanobedbId: string | null = null;
|
||||||
let urnKoboId: string | null = null;
|
let urnKoboId: string | null = null;
|
||||||
@@ -300,6 +317,9 @@ export function parseOpf(xml: string): ParsedOpf {
|
|||||||
if (scheme === 'amazon') schemeAmazonId ??= value || null;
|
if (scheme === 'amazon') schemeAmazonId ??= value || null;
|
||||||
if (scheme === 'goodreads') schemeGoodreadsId ??= value || null;
|
if (scheme === 'goodreads') schemeGoodreadsId ??= value || null;
|
||||||
if (scheme === 'hardcover') schemeHardcoverId ??= value || null;
|
if (scheme === 'hardcover') schemeHardcoverId ??= value || null;
|
||||||
|
if (scheme === 'hardcover_edition' || scheme === 'hardcover-edition' || scheme === 'hardcoveredition') {
|
||||||
|
schemeHardcoverEditionId ??= value || null;
|
||||||
|
}
|
||||||
if (scheme === 'openlibrary') schemeOpenLibraryId ??= value || null;
|
if (scheme === 'openlibrary') schemeOpenLibraryId ??= value || null;
|
||||||
if (scheme === 'ranobedb') schemeRanobedbId ??= value || null;
|
if (scheme === 'ranobedb') schemeRanobedbId ??= value || null;
|
||||||
if (scheme === 'kobo') schemeKoboId ??= value || null;
|
if (scheme === 'kobo') schemeKoboId ??= value || null;
|
||||||
@@ -311,6 +331,8 @@ export function parseOpf(xml: string): ParsedOpf {
|
|||||||
if (value.startsWith('urn:goodreads:')) urnGoodreadsId ??= value.slice('urn:goodreads:'.length) || null;
|
if (value.startsWith('urn:goodreads:')) urnGoodreadsId ??= value.slice('urn:goodreads:'.length) || null;
|
||||||
if (value.startsWith('urn:amazon:')) urnAmazonId ??= value.slice('urn:amazon:'.length) || null;
|
if (value.startsWith('urn:amazon:')) urnAmazonId ??= value.slice('urn:amazon:'.length) || null;
|
||||||
if (value.startsWith('urn:hardcover:')) urnHardcoverId ??= value.slice('urn:hardcover:'.length) || null;
|
if (value.startsWith('urn:hardcover:')) urnHardcoverId ??= value.slice('urn:hardcover:'.length) || null;
|
||||||
|
if (value.startsWith('urn:hardcover_edition:')) urnHardcoverEditionId ??= value.slice('urn:hardcover_edition:'.length) || null;
|
||||||
|
if (value.startsWith('urn:hardcover-edition:')) urnHardcoverEditionId ??= value.slice('urn:hardcover-edition:'.length) || null;
|
||||||
if (value.startsWith('urn:google:')) urnGoogleBooksId ??= value.slice('urn:google:'.length) || null;
|
if (value.startsWith('urn:google:')) urnGoogleBooksId ??= value.slice('urn:google:'.length) || null;
|
||||||
if (value.startsWith('urn:openlibrary:')) urnOpenLibraryId ??= value.slice('urn:openlibrary:'.length) || null;
|
if (value.startsWith('urn:openlibrary:')) urnOpenLibraryId ??= value.slice('urn:openlibrary:'.length) || null;
|
||||||
if (value.startsWith('urn:ranobedb:')) urnRanobedbId ??= value.slice('urn:ranobedb:'.length) || null;
|
if (value.startsWith('urn:ranobedb:')) urnRanobedbId ??= value.slice('urn:ranobedb:'.length) || null;
|
||||||
@@ -337,6 +359,7 @@ export function parseOpf(xml: string): ParsedOpf {
|
|||||||
const goodreadsId = schemeGoodreadsId ?? urnGoodreadsId ?? prefixIds.goodreads ?? null;
|
const goodreadsId = schemeGoodreadsId ?? urnGoodreadsId ?? prefixIds.goodreads ?? null;
|
||||||
const amazonId = schemeAmazonId ?? urnAmazonId ?? prefixIds.amazon ?? null;
|
const amazonId = schemeAmazonId ?? urnAmazonId ?? prefixIds.amazon ?? null;
|
||||||
const hardcoverId = schemeHardcoverId ?? urnHardcoverId ?? prefixIds.hardcover ?? null;
|
const hardcoverId = schemeHardcoverId ?? urnHardcoverId ?? prefixIds.hardcover ?? null;
|
||||||
|
const hardcoverEditionId = schemeHardcoverEditionId ?? urnHardcoverEditionId ?? prefixIds.hardcoverEdition ?? null;
|
||||||
const openLibraryId = schemeOpenLibraryId ?? urnOpenLibraryId ?? prefixIds.openlibrary ?? null;
|
const openLibraryId = schemeOpenLibraryId ?? urnOpenLibraryId ?? prefixIds.openlibrary ?? null;
|
||||||
const ranobedbId = schemeRanobedbId ?? urnRanobedbId ?? prefixIds.ranobedb ?? null;
|
const ranobedbId = schemeRanobedbId ?? urnRanobedbId ?? prefixIds.ranobedb ?? null;
|
||||||
const koboId = schemeKoboId ?? urnKoboId ?? prefixIds.kobo ?? null;
|
const koboId = schemeKoboId ?? urnKoboId ?? prefixIds.kobo ?? null;
|
||||||
@@ -461,6 +484,7 @@ export function parseOpf(xml: string): ParsedOpf {
|
|||||||
goodreadsId,
|
goodreadsId,
|
||||||
amazonId,
|
amazonId,
|
||||||
hardcoverId,
|
hardcoverId,
|
||||||
|
hardcoverEditionId,
|
||||||
openLibraryId,
|
openLibraryId,
|
||||||
ranobedbId,
|
ranobedbId,
|
||||||
koboId,
|
koboId,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ describe('parsePdfFileInWorker', () => {
|
|||||||
goodreadsId: null,
|
goodreadsId: null,
|
||||||
amazonId: null,
|
amazonId: null,
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
openLibraryId: null,
|
openLibraryId: null,
|
||||||
ranobedbId: null,
|
ranobedbId: null,
|
||||||
koboId: null,
|
koboId: null,
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ function makeParsed(overrides: Partial<PdfParsed> = {}): PdfParsed {
|
|||||||
goodreadsId: null,
|
goodreadsId: null,
|
||||||
amazonId: null,
|
amazonId: null,
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
openLibraryId: null,
|
openLibraryId: null,
|
||||||
ranobedbId: null,
|
ranobedbId: null,
|
||||||
koboId: null,
|
koboId: null,
|
||||||
@@ -119,6 +120,7 @@ describe('parsePdfFile', () => {
|
|||||||
goodreadsId: 'gr1',
|
goodreadsId: 'gr1',
|
||||||
amazonId: 'a1',
|
amazonId: 'a1',
|
||||||
hardcoverId: 'h1',
|
hardcoverId: 'h1',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
openLibraryId: 'ol1',
|
openLibraryId: 'ol1',
|
||||||
ranobedbId: 'ranobe-1',
|
ranobedbId: 'ranobe-1',
|
||||||
itunesId: 'it1',
|
itunesId: 'it1',
|
||||||
@@ -131,6 +133,7 @@ describe('parsePdfFile', () => {
|
|||||||
title: 'XMP Title',
|
title: 'XMP Title',
|
||||||
authors: [{ name: 'XMP Author', sortName: null }],
|
authors: [{ name: 'XMP Author', sortName: null }],
|
||||||
tags: ['favorite'],
|
tags: ['favorite'],
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
ranobedbId: 'ranobe-1',
|
ranobedbId: 'ranobe-1',
|
||||||
pageCount: 999,
|
pageCount: 999,
|
||||||
coverBuffer: Buffer.from([0xff, 0xd8, 0xff]),
|
coverBuffer: Buffer.from([0xff, 0xd8, 0xff]),
|
||||||
@@ -277,6 +280,7 @@ describe('parsePdfFile', () => {
|
|||||||
goodreadsId: null,
|
goodreadsId: null,
|
||||||
amazonId: null,
|
amazonId: null,
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
openLibraryId: null,
|
openLibraryId: null,
|
||||||
ranobedbId: null,
|
ranobedbId: null,
|
||||||
itunesId: null,
|
itunesId: null,
|
||||||
@@ -315,6 +319,7 @@ describe('parsePdfFile', () => {
|
|||||||
goodreadsId: null,
|
goodreadsId: null,
|
||||||
amazonId: null,
|
amazonId: null,
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
openLibraryId: null,
|
openLibraryId: null,
|
||||||
ranobedbId: null,
|
ranobedbId: null,
|
||||||
itunesId: null,
|
itunesId: null,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface PdfParsed {
|
|||||||
goodreadsId: string | null;
|
goodreadsId: string | null;
|
||||||
amazonId: string | null;
|
amazonId: string | null;
|
||||||
hardcoverId: string | null;
|
hardcoverId: string | null;
|
||||||
|
hardcoverEditionId: string | null;
|
||||||
openLibraryId: string | null;
|
openLibraryId: string | null;
|
||||||
ranobedbId: string | null;
|
ranobedbId: string | null;
|
||||||
koboId: string | null;
|
koboId: string | null;
|
||||||
@@ -158,6 +159,7 @@ export async function parsePdfBuffer(absolutePath: string, buf: Buffer, options:
|
|||||||
goodreadsId: xmp?.goodreadsId ?? null,
|
goodreadsId: xmp?.goodreadsId ?? null,
|
||||||
amazonId: xmp?.amazonId ?? null,
|
amazonId: xmp?.amazonId ?? null,
|
||||||
hardcoverId: xmp?.hardcoverId ?? null,
|
hardcoverId: xmp?.hardcoverId ?? null,
|
||||||
|
hardcoverEditionId: xmp?.hardcoverEditionId ?? null,
|
||||||
openLibraryId: xmp?.openLibraryId ?? null,
|
openLibraryId: xmp?.openLibraryId ?? null,
|
||||||
ranobedbId: xmp?.ranobedbId ?? null,
|
ranobedbId: xmp?.ranobedbId ?? null,
|
||||||
koboId: xmp?.koboId ?? null,
|
koboId: xmp?.koboId ?? null,
|
||||||
|
|||||||
@@ -192,6 +192,11 @@ describe('parseXmp', () => {
|
|||||||
const r = parseXmp(xmpDoc('<bookorbit:amazonId>B001234567</bookorbit:amazonId>'));
|
const r = parseXmp(xmpDoc('<bookorbit:amazonId>B001234567</bookorbit:amazonId>'));
|
||||||
expect(r?.amazonId).toBe('B001234567');
|
expect(r?.amazonId).toBe('B001234567');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses bookorbit:hardcoverEditionId', () => {
|
||||||
|
const r = parseXmp(xmpDoc('<bookorbit:hardcoverEditionId>8941973</bookorbit:hardcoverEditionId>'));
|
||||||
|
expect(r?.hardcoverEditionId).toBe('8941973');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('multiple rdf:Description blocks', () => {
|
describe('multiple rdf:Description blocks', () => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export interface XmpParsed {
|
|||||||
goodreadsId: string | null;
|
goodreadsId: string | null;
|
||||||
amazonId: string | null;
|
amazonId: string | null;
|
||||||
hardcoverId: string | null;
|
hardcoverId: string | null;
|
||||||
|
hardcoverEditionId: string | null;
|
||||||
openLibraryId: string | null;
|
openLibraryId: string | null;
|
||||||
ranobedbId: string | null;
|
ranobedbId: string | null;
|
||||||
koboId: string | null;
|
koboId: string | null;
|
||||||
@@ -158,6 +159,7 @@ export function parseXmp(xmpXml: string): XmpParsed | null {
|
|||||||
goodreadsId: str(merged[`${px}:goodreadsId`]),
|
goodreadsId: str(merged[`${px}:goodreadsId`]),
|
||||||
amazonId: str(merged[`${px}:amazonId`]),
|
amazonId: str(merged[`${px}:amazonId`]),
|
||||||
hardcoverId: str(merged[`${px}:hardcoverId`]),
|
hardcoverId: str(merged[`${px}:hardcoverId`]),
|
||||||
|
hardcoverEditionId: str(merged[`${px}:hardcoverEditionId`]),
|
||||||
openLibraryId: str(merged[`${px}:openLibraryId`]),
|
openLibraryId: str(merged[`${px}:openLibraryId`]),
|
||||||
ranobedbId: str(merged[`${px}:ranobedbId`]),
|
ranobedbId: str(merged[`${px}:ranobedbId`]),
|
||||||
koboId: str(merged[`${px}:koboId`]),
|
koboId: str(merged[`${px}:koboId`]),
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ describe('MetadataService', () => {
|
|||||||
goodreadsId: null,
|
goodreadsId: null,
|
||||||
amazonId: null,
|
amazonId: null,
|
||||||
hardcoverId: null,
|
hardcoverId: null,
|
||||||
|
hardcoverEditionId: null,
|
||||||
openLibraryId: null,
|
openLibraryId: null,
|
||||||
ranobedbId: null,
|
ranobedbId: null,
|
||||||
itunesId: null,
|
itunesId: null,
|
||||||
@@ -566,6 +567,7 @@ describe('MetadataService', () => {
|
|||||||
goodreadsId: 'goodreads-1',
|
goodreadsId: 'goodreads-1',
|
||||||
amazonId: 'amazon-1',
|
amazonId: 'amazon-1',
|
||||||
hardcoverId: 'hardcover-1',
|
hardcoverId: 'hardcover-1',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
openLibraryId: 'open-library-1',
|
openLibraryId: 'open-library-1',
|
||||||
ranobedbId: 'ranobe-1',
|
ranobedbId: 'ranobe-1',
|
||||||
itunesId: 'itunes-1',
|
itunesId: 'itunes-1',
|
||||||
@@ -595,6 +597,7 @@ describe('MetadataService', () => {
|
|||||||
goodreadsId: 'goodreads-1',
|
goodreadsId: 'goodreads-1',
|
||||||
amazonId: 'amazon-1',
|
amazonId: 'amazon-1',
|
||||||
hardcoverId: 'hardcover-1',
|
hardcoverId: 'hardcover-1',
|
||||||
|
hardcoverEditionId: '8941973',
|
||||||
openLibraryId: 'open-library-1',
|
openLibraryId: 'open-library-1',
|
||||||
ranobedbId: 'ranobe-1',
|
ranobedbId: 'ranobe-1',
|
||||||
itunesId: 'itunes-1',
|
itunesId: 'itunes-1',
|
||||||
|
|||||||
@@ -587,6 +587,7 @@ export class MetadataService {
|
|||||||
goodreadsId: data.goodreadsId,
|
goodreadsId: data.goodreadsId,
|
||||||
amazonId: data.amazonId,
|
amazonId: data.amazonId,
|
||||||
hardcoverId: data.hardcoverId,
|
hardcoverId: data.hardcoverId,
|
||||||
|
hardcoverEditionId: data.hardcoverEditionId,
|
||||||
openLibraryId: data.openLibraryId,
|
openLibraryId: data.openLibraryId,
|
||||||
ranobedbId: data.ranobedbId,
|
ranobedbId: data.ranobedbId,
|
||||||
koboId: data.koboId,
|
koboId: data.koboId,
|
||||||
@@ -613,6 +614,7 @@ export class MetadataService {
|
|||||||
if (filtered.goodreadsId !== undefined) scalarFields.goodreadsId = filtered.goodreadsId;
|
if (filtered.goodreadsId !== undefined) scalarFields.goodreadsId = filtered.goodreadsId;
|
||||||
if (filtered.amazonId !== undefined) scalarFields.amazonId = filtered.amazonId;
|
if (filtered.amazonId !== undefined) scalarFields.amazonId = filtered.amazonId;
|
||||||
if (filtered.hardcoverId !== undefined) scalarFields.hardcoverId = filtered.hardcoverId;
|
if (filtered.hardcoverId !== undefined) scalarFields.hardcoverId = filtered.hardcoverId;
|
||||||
|
if (filtered.hardcoverEditionId !== undefined) scalarFields.hardcoverEditionId = filtered.hardcoverEditionId;
|
||||||
if (filtered.openLibraryId !== undefined) scalarFields.openLibraryId = filtered.openLibraryId;
|
if (filtered.openLibraryId !== undefined) scalarFields.openLibraryId = filtered.openLibraryId;
|
||||||
if (filtered.ranobedbId !== undefined) scalarFields.ranobedbId = filtered.ranobedbId;
|
if (filtered.ranobedbId !== undefined) scalarFields.ranobedbId = filtered.ranobedbId;
|
||||||
if (filtered.koboId !== undefined) scalarFields.koboId = filtered.koboId;
|
if (filtered.koboId !== undefined) scalarFields.koboId = filtered.koboId;
|
||||||
|
|||||||
@@ -548,6 +548,8 @@ export class ScannerRepository {
|
|||||||
publisher: bookMetadata.publisher,
|
publisher: bookMetadata.publisher,
|
||||||
pageCount: bookMetadata.pageCount,
|
pageCount: bookMetadata.pageCount,
|
||||||
isbn13: bookMetadata.isbn13,
|
isbn13: bookMetadata.isbn13,
|
||||||
|
hardcoverId: bookMetadata.hardcoverId,
|
||||||
|
hardcoverEditionId: bookMetadata.hardcoverEditionId,
|
||||||
})
|
})
|
||||||
.from(books)
|
.from(books)
|
||||||
.leftJoin(bookMetadata, eq(bookMetadata.bookId, books.id))
|
.leftJoin(bookMetadata, eq(bookMetadata.bookId, books.id))
|
||||||
|
|||||||
Reference in New Issue
Block a user