mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat(metadata): add Aladin Korean book metadata provider (#351)
* feat(metadata): add Aladin Korean book metadata provider Add support for fetching Korean book metadata from Aladin OpenAPI. Includes search by title/author/ISBN and lookup by ISBN-10/13. - Add ALADIN to MetadataProviderKey in @bookorbit/types - Add AladinProviderConfig to ProviderConfigurations - Create AladinProvider with IdentifiableProvider interface - Add mapper for Aladin API response to MetadataCandidate - Register provider in MetadataFetchModule - Add provider config merge, validation, and connection test - Include 24 unit tests (provider + mapper) * feat(client): add Aladin provider UI integration * refactor(metadata): use 'Aladin' label and favicon-based icon * feat(metadata): complete Aladin provider end-to-end wiring Persist the Aladin ItemId across fetch, scoring, locks, and file metadata round-trip, and update the affected tests. Co-authored-by: Kyungwook Park <288273+kyungw00k@users.noreply.github.com> --------- Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com> Co-authored-by: Kyungwook Park <288273+kyungw00k@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><title>Aladin</title><image width="48" height="48" href="data:image/png;base64,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"/></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -154,6 +154,15 @@ const providerLinks = computed<ProviderLink[]>(() => {
|
||||
fallback: 'LC',
|
||||
})
|
||||
}
|
||||
if (ids.aladin) {
|
||||
out.push({
|
||||
key: 'aladin',
|
||||
label: 'Aladin',
|
||||
url: `https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=${ids.aladin}`,
|
||||
iconUrl: providerIconPath('aladin'),
|
||||
fallback: '알',
|
||||
})
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
|
||||
@@ -612,6 +612,15 @@ const providerLinks = computed<ProviderLink[]>(() => {
|
||||
fallback: 'LC',
|
||||
})
|
||||
}
|
||||
if (ids.aladin) {
|
||||
out.push({
|
||||
key: 'aladin',
|
||||
label: 'Aladin',
|
||||
url: `https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=${ids.aladin}`,
|
||||
iconUrl: providerIconPath('aladin'),
|
||||
fallback: '알',
|
||||
})
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ const DIRECT_PATCH_FIELDS = [
|
||||
'comicvineId',
|
||||
'ranobedbId',
|
||||
'lubimyczytacId',
|
||||
'aladinId',
|
||||
] as const
|
||||
|
||||
const COMIC_FIELD_MAP = {
|
||||
@@ -173,6 +174,7 @@ const providerIdFields = [
|
||||
{ field: 'comicvineId' as const, label: 'ComicVine' },
|
||||
{ field: 'ranobedbId' as const, label: 'RanobeDB' },
|
||||
{ field: 'lubimyczytacId' as const, label: 'LubimyCzytac' },
|
||||
{ field: 'aladinId' as const, label: 'Aladin' },
|
||||
]
|
||||
|
||||
function setIntField(field: 'publishedYear' | 'pageCount' | 'durationSeconds', e: Event) {
|
||||
@@ -463,6 +465,7 @@ function buildPreviewPatch(preview: MetadataRefreshPreview): MetadataPatch {
|
||||
comicvineId: preview.comicvineId,
|
||||
ranobedbId: preview.ranobedbId,
|
||||
lubimyczytacId: preview.lubimyczytacId,
|
||||
aladinId: preview.aladinId,
|
||||
comicMetadata: preview.comicMetadata,
|
||||
narrators: preview.audioMetadata?.narrators,
|
||||
durationSeconds: preview.audioMetadata?.durationSeconds ?? undefined,
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface FileMetadata {
|
||||
comicvineId?: string | null
|
||||
ranobedbId?: string | null
|
||||
lubimyczytacId?: string | null
|
||||
aladinId?: string | null
|
||||
authors?: string[]
|
||||
genres?: string[]
|
||||
narrators?: string[]
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface MetadataPatch {
|
||||
comicvineId?: string | null
|
||||
ranobedbId?: string | null
|
||||
lubimyczytacId?: string | null
|
||||
aladinId?: string | null
|
||||
comicMetadata?: ComicMetadataFields
|
||||
}
|
||||
|
||||
@@ -160,6 +161,7 @@ export type ProviderIdPatchField =
|
||||
| 'comicvineId'
|
||||
| 'ranobedbId'
|
||||
| 'lubimyczytacId'
|
||||
| 'aladinId'
|
||||
|
||||
export const PROVIDER_ID_FIELD: Record<MetadataProviderKey, ProviderIdPatchField | undefined> = {
|
||||
google: 'googleBooksId',
|
||||
@@ -174,6 +176,7 @@ export const PROVIDER_ID_FIELD: Record<MetadataProviderKey, ProviderIdPatchField
|
||||
ranobedb: 'ranobedbId',
|
||||
kobo: 'koboId',
|
||||
lubimyczytac: 'lubimyczytacId',
|
||||
aladin: 'aladinId',
|
||||
}
|
||||
|
||||
const PROVIDER_ID_PATCH_FIELDS = new Set<string>(Object.values(PROVIDER_ID_FIELD).filter((v): v is ProviderIdPatchField => v !== undefined))
|
||||
@@ -195,6 +198,7 @@ export const PROVIDER_ID_LABEL: Record<MetadataProviderKey, string> = {
|
||||
ranobedb: 'RanobeDB ID',
|
||||
kobo: 'Kobo ID',
|
||||
lubimyczytac: 'LubimyCzytac ID',
|
||||
aladin: 'Aladin ID',
|
||||
}
|
||||
|
||||
export function getCandidateValueFrom(candidate: MetadataCandidate, key: DiffFieldKey): string {
|
||||
|
||||
@@ -32,6 +32,7 @@ const ROOT_FIELDS = [
|
||||
'comicvineId',
|
||||
'ranobedbId',
|
||||
'lubimyczytacId',
|
||||
'aladinId',
|
||||
] as const
|
||||
|
||||
const COMIC_FIELDS = {
|
||||
@@ -115,6 +116,7 @@ export function useMetadataEditor() {
|
||||
comicvineId: null as string | null,
|
||||
ranobedbId: null as string | null,
|
||||
lubimyczytacId: null as string | null,
|
||||
aladinId: null as string | null,
|
||||
comicIssueNumber: null as string | null,
|
||||
comicVolumeName: null as string | null,
|
||||
comicStoryArcs: [] as string[],
|
||||
@@ -164,6 +166,7 @@ export function useMetadataEditor() {
|
||||
form.comicvineId = book.providerIds.comicvine ?? null
|
||||
form.ranobedbId = book.providerIds.ranobedb ?? null
|
||||
form.lubimyczytacId = book.providerIds.lubimyczytac ?? null
|
||||
form.aladinId = book.providerIds.aladin ?? null
|
||||
const cm = book.comicMetadata
|
||||
form.comicIssueNumber = cm?.issueNumber ?? null
|
||||
form.comicVolumeName = cm?.volumeName ?? null
|
||||
|
||||
@@ -96,6 +96,7 @@ export function useMetadataSearch() {
|
||||
'itunes',
|
||||
'kobo',
|
||||
'openLibrary',
|
||||
'aladin',
|
||||
]
|
||||
|
||||
function sortResults(list: MetadataCandidate[]): MetadataCandidate[] {
|
||||
|
||||
@@ -24,6 +24,7 @@ const FILE_METADATA_PATCH_FIELDS = [
|
||||
'comicvineId',
|
||||
'ranobedbId',
|
||||
'lubimyczytacId',
|
||||
'aladinId',
|
||||
'authors',
|
||||
'genres',
|
||||
'narrators',
|
||||
|
||||
@@ -18,6 +18,7 @@ const PROVIDER_LABELS: Record<MetadataProviderKeyType, string> = {
|
||||
[MetadataProviderKey.RANOBEDB]: 'RanobeDB',
|
||||
[MetadataProviderKey.KOBO]: 'Kobo',
|
||||
[MetadataProviderKey.LUBIMYCZYTAC]: 'LubimyCzytac',
|
||||
[MetadataProviderKey.ALADIN]: 'Aladin',
|
||||
}
|
||||
|
||||
export function metadataRefreshEmptyMessage(diagnostics: MetadataFetchDiagnostics, book: BookDetail): string {
|
||||
|
||||
@@ -11,6 +11,7 @@ const PROVIDER_ICON_PATHS = {
|
||||
kobo: `${PROVIDER_ICON_BASE_PATH}/kobo.svg`,
|
||||
ranobedb: `${PROVIDER_ICON_BASE_PATH}/ranobedb.svg`,
|
||||
lubimyczytac: `${PROVIDER_ICON_BASE_PATH}/lubimyczytac.svg`,
|
||||
aladin: `${PROVIDER_ICON_BASE_PATH}/aladin.svg`,
|
||||
} as const
|
||||
|
||||
type ProviderIconKey = keyof typeof PROVIDER_ICON_PATHS
|
||||
|
||||
@@ -64,6 +64,8 @@ function isFieldFilled(field: MetadataScoreField): boolean {
|
||||
return !!b.providerIds.openLibrary?.trim()
|
||||
case 'itunesId':
|
||||
return !!b.providerIds.itunes?.trim()
|
||||
case 'aladinId':
|
||||
return !!b.providerIds.aladin?.trim()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
+11
-1
@@ -204,9 +204,19 @@ const rows: RowDef[] = [
|
||||
hint: 'Polish book catalog (lubimyczytac.pl). Scrapes public book pages. No setup required.',
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
key: 'aladin',
|
||||
label: 'Aladin',
|
||||
hint: 'Korean book metadata from Aladin (알라딘). A TTB Key from aladin.co.kr is required.',
|
||||
fields: [{ key: 'ttbKey', label: 'TTB Key', type: 'password', placeholder: 'ttb...', alwaysEditable: true }],
|
||||
enableRequirement: {
|
||||
isConfigured: (c) => !!c.aladin.ttbKey.trim(),
|
||||
blockedMessage: 'Aladin requires a TTB Key before it can be enabled',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const TESTABLE_PROVIDERS: MetadataProviderKey[] = [MetadataProviderKey.AMAZON, MetadataProviderKey.HARDCOVER]
|
||||
const TESTABLE_PROVIDERS: MetadataProviderKey[] = [MetadataProviderKey.AMAZON, MetadataProviderKey.HARDCOVER, MetadataProviderKey.ALADIN]
|
||||
|
||||
function statusFor(key: string) {
|
||||
return props.statuses.find((s) => s.key === key)
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('getProviderColor', () => {
|
||||
'comicvine',
|
||||
'ranobedb',
|
||||
'kobo',
|
||||
'aladin',
|
||||
'auto',
|
||||
]
|
||||
for (const provider of providers) {
|
||||
@@ -46,7 +47,20 @@ describe('PROVIDER_SHORT_LABELS', () => {
|
||||
})
|
||||
|
||||
it('has labels for all providers', () => {
|
||||
const providers = ['google', 'amazon', 'goodreads', 'hardcover', 'openLibrary', 'itunes', 'audible', 'audnexus', 'comicvine', 'ranobedb', 'kobo']
|
||||
const providers = [
|
||||
'google',
|
||||
'amazon',
|
||||
'goodreads',
|
||||
'hardcover',
|
||||
'openLibrary',
|
||||
'itunes',
|
||||
'audible',
|
||||
'audnexus',
|
||||
'comicvine',
|
||||
'ranobedb',
|
||||
'kobo',
|
||||
'aladin',
|
||||
]
|
||||
for (const provider of providers) {
|
||||
expect(PROVIDER_SHORT_LABELS[provider]).toBeTruthy()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ const PROVIDER_HEX: Record<string, string> = {
|
||||
ranobedb: '#a78cff',
|
||||
kobo: '#e23434',
|
||||
lubimyczytac: '#f47373',
|
||||
aladin: '#3E7FFF',
|
||||
auto: '#8B5CF6',
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ export const PROVIDER_SHORT_LABELS: Record<string, string> = {
|
||||
ranobedb: 'RanobeDB',
|
||||
kobo: 'Kobo',
|
||||
lubimyczytac: 'LubimyCzytac',
|
||||
aladin: 'Aladin',
|
||||
auto: 'Fetched',
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user