mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
fix(metadata): show ComicVine links on book details
Build provider links from one shared registry so the detail page and quick view stay aligned.\nRender ComicVine with its CV fallback when no icon is available. Fixes #1103
This commit is contained in:
@@ -8,8 +8,7 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { DialogRoot, DialogContent, DialogPortal, DialogOverlay, DialogClose, DialogTitle, DialogDescription } from 'reka-ui'
|
||||
import { formatBytes } from '@/lib/formatting'
|
||||
import { getProviderColor } from '@/lib/provider-colors'
|
||||
import { providerIconPath } from '@/features/book/lib/provider-icons'
|
||||
import { libroFmAudiobookUrl, lubimyczytacBookUrl } from '@/features/book/lib/provider-links'
|
||||
import { createBookProviderLinks } from '@/features/book/lib/provider-links'
|
||||
import { useBookDetail } from '../composables/useBookDetail'
|
||||
import { useCoverVersions } from '../composables/useCoverVersions'
|
||||
import { getFormatColor } from '../lib/format-colors'
|
||||
@@ -32,14 +31,6 @@ const emit = defineEmits<{
|
||||
action: [type: 'add-to-collection' | 'delete']
|
||||
}>()
|
||||
|
||||
type ProviderLink = {
|
||||
key: string
|
||||
label: string
|
||||
url: string
|
||||
iconUrl: string
|
||||
fallback: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const { detail, loading, fetch } = useBookDetail()
|
||||
|
||||
@@ -81,103 +72,7 @@ const seriesLine = computed(() => {
|
||||
|
||||
const authorLine = computed(() => detail.value?.authors.map((a) => a.name).join(', ') ?? null)
|
||||
const ratingStars = [1, 2, 3, 4, 5]
|
||||
const providerLinks = computed<ProviderLink[]>(() => {
|
||||
const out: ProviderLink[] = []
|
||||
const ids = detail.value?.providerIds
|
||||
if (!ids) return out
|
||||
if (ids.google) {
|
||||
out.push({
|
||||
key: 'google',
|
||||
label: 'Google Books',
|
||||
url: `https://books.google.com/books?id=${ids.google}`,
|
||||
iconUrl: providerIconPath('google'),
|
||||
fallback: 'G',
|
||||
})
|
||||
}
|
||||
if (ids.goodreads) {
|
||||
out.push({
|
||||
key: 'goodreads',
|
||||
label: 'Goodreads',
|
||||
url: `https://www.goodreads.com/book/show/${ids.goodreads}`,
|
||||
iconUrl: providerIconPath('goodreads'),
|
||||
fallback: 'GR',
|
||||
})
|
||||
}
|
||||
if (ids.amazon) {
|
||||
out.push({
|
||||
key: 'amazon',
|
||||
label: 'Amazon',
|
||||
url: `https://www.amazon.com/dp/${ids.amazon}`,
|
||||
iconUrl: providerIconPath('amazon'),
|
||||
fallback: 'A',
|
||||
})
|
||||
}
|
||||
if (ids.hardcover) {
|
||||
out.push({
|
||||
key: 'hardcover',
|
||||
label: 'Hardcover',
|
||||
url: `https://hardcover.app/books/${ids.hardcover}`,
|
||||
iconUrl: providerIconPath('hardcover'),
|
||||
fallback: 'H',
|
||||
})
|
||||
}
|
||||
if (ids.openLibrary) {
|
||||
const path = String(ids.openLibrary).startsWith('/works/') ? String(ids.openLibrary) : `/works/${ids.openLibrary}`
|
||||
out.push({
|
||||
key: 'openLibrary',
|
||||
label: 'Open Library',
|
||||
url: `https://openlibrary.org${path}`,
|
||||
iconUrl: providerIconPath('openLibrary'),
|
||||
fallback: 'OL',
|
||||
})
|
||||
}
|
||||
if (ids.itunes) {
|
||||
out.push({
|
||||
key: 'itunes',
|
||||
label: 'Apple Books',
|
||||
url: `https://books.apple.com/book/id${ids.itunes}`,
|
||||
iconUrl: providerIconPath('itunes'),
|
||||
fallback: '',
|
||||
})
|
||||
}
|
||||
if (ids.librofm) {
|
||||
out.push({
|
||||
key: 'librofm',
|
||||
label: 'Libro.fm',
|
||||
url: libroFmAudiobookUrl(ids.librofm),
|
||||
iconUrl: providerIconPath('librofm'),
|
||||
fallback: 'Lf',
|
||||
})
|
||||
}
|
||||
if (ids.ranobedb) {
|
||||
out.push({
|
||||
key: 'ranobedb',
|
||||
label: 'RanobeDB',
|
||||
url: `https://ranobedb.org/book/${ids.ranobedb}`,
|
||||
iconUrl: providerIconPath('ranobedb'),
|
||||
fallback: 'RN',
|
||||
})
|
||||
}
|
||||
if (ids.lubimyczytac) {
|
||||
out.push({
|
||||
key: 'lubimyczytac',
|
||||
label: 'LubimyCzytac',
|
||||
url: lubimyczytacBookUrl(ids.lubimyczytac),
|
||||
iconUrl: providerIconPath('lubimyczytac'),
|
||||
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
|
||||
})
|
||||
const providerLinks = computed(() => (detail.value ? createBookProviderLinks(detail.value.providerIds) : []))
|
||||
|
||||
const safeDescription = useSafeHtml(() => detail.value?.description)
|
||||
|
||||
@@ -351,7 +246,7 @@ function handleDelete() {
|
||||
:style="providerLinkStyle(link.key)"
|
||||
>
|
||||
<img
|
||||
v-if="!providerIconErrors[link.key]"
|
||||
v-if="link.iconUrl && !providerIconErrors[link.key]"
|
||||
:src="link.iconUrl"
|
||||
:alt="link.label"
|
||||
class="size-3.5 rounded-[2px] object-contain"
|
||||
|
||||
@@ -227,6 +227,25 @@ describe('BookQuickView', () => {
|
||||
expect(ranobedbLink.find('img[alt="RanobeDB"][src="/assets/provider-icons/ranobedb.svg"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders a direct ComicVine issue link with the monogram fallback', () => {
|
||||
detailRef.value = makeDetail({ providerIds: { comicvine: '1126983' } })
|
||||
|
||||
const wrapper = mount(BookQuickView, {
|
||||
props: { open: true, bookId: 42 },
|
||||
global: globalStubs,
|
||||
})
|
||||
|
||||
const comicVineLink = wrapper.find('a[title="Open in ComicVine"]')
|
||||
expect(comicVineLink.exists()).toBe(true)
|
||||
expect(comicVineLink.attributes()).toMatchObject({
|
||||
href: 'https://comicvine.gamespot.com/issue/4000-1126983/',
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
})
|
||||
expect(comicVineLink.find('img').exists()).toBe(false)
|
||||
expect(comicVineLink.text()).toBe('CV')
|
||||
})
|
||||
|
||||
it('renders Libro.fm provider icon link', () => {
|
||||
detailRef.value = makeDetail({ providerIds: { librofm: '9781234567890' } })
|
||||
|
||||
|
||||
@@ -198,6 +198,29 @@ describe('DetailsTab - present state', () => {
|
||||
expect(wrapper.text()).not.toContain('Info Links')
|
||||
})
|
||||
|
||||
it('renders a direct ComicVine issue link with the monogram fallback', () => {
|
||||
const wrapper = mount(DetailsTab, {
|
||||
props: {
|
||||
book: makeBook({
|
||||
providerIds: {
|
||||
comicvine: '1126983',
|
||||
},
|
||||
}),
|
||||
},
|
||||
global: globalStubs,
|
||||
})
|
||||
|
||||
const comicVineLink = wrapper.find('a[title="Open in ComicVine"]')
|
||||
expect(comicVineLink.exists()).toBe(true)
|
||||
expect(comicVineLink.attributes()).toMatchObject({
|
||||
href: 'https://comicvine.gamespot.com/issue/4000-1126983/',
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
})
|
||||
expect(comicVineLink.find('img').exists()).toBe(false)
|
||||
expect(comicVineLink.text()).toBe('CV')
|
||||
})
|
||||
|
||||
it('fetches collections data for Kobo sync but does not display them', async () => {
|
||||
vi.mocked(api).mockImplementation(async (input) => {
|
||||
if (input === '/api/v1/collections/membership') {
|
||||
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
} from '@lucide/vue'
|
||||
import { DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogRoot } from 'reka-ui'
|
||||
import { getFormatColor } from '@/features/book/lib/format-colors'
|
||||
import { providerIconPath, providerIconPathSafe } from '@/features/book/lib/provider-icons'
|
||||
import { libroFmAudiobookUrl, lubimyczytacBookUrl } from '@/features/book/lib/provider-links'
|
||||
import { providerIconPathSafe } from '@/features/book/lib/provider-icons'
|
||||
import { createBookProviderLinks } from '@/features/book/lib/provider-links'
|
||||
import { getProviderColor, PROVIDER_SHORT_LABELS } from '@/lib/provider-colors'
|
||||
import { useCoverVersions } from '@/features/book/composables/useCoverVersions'
|
||||
import { COVER_ASPECT_RATIO_KEY, DEFAULT_COVER_ASPECT_RATIO } from '@/features/book/lib/cover-aspect-ratio'
|
||||
@@ -81,14 +81,6 @@ type CollectionMembership = {
|
||||
memberCount?: number
|
||||
}
|
||||
|
||||
type ProviderLink = {
|
||||
key: string
|
||||
label: string
|
||||
url: string
|
||||
iconUrl: string
|
||||
fallback: string
|
||||
}
|
||||
|
||||
type SeriesDisplayLink = {
|
||||
key: string
|
||||
seriesId: number | null
|
||||
@@ -655,120 +647,7 @@ const supplementalLoading = ref(false)
|
||||
const resettingFileIds = ref<number[]>([])
|
||||
const providerIconErrors = ref<Record<string, boolean>>({})
|
||||
|
||||
const providerLinks = computed<ProviderLink[]>(() => {
|
||||
const out: ProviderLink[] = []
|
||||
const ids = props.book.providerIds
|
||||
if (ids.google) {
|
||||
out.push({
|
||||
key: 'google',
|
||||
label: 'Google Books',
|
||||
url: `https://books.google.com/books?id=${ids.google}`,
|
||||
iconUrl: providerIconPath('google'),
|
||||
fallback: 'G',
|
||||
})
|
||||
}
|
||||
if (ids.goodreads) {
|
||||
out.push({
|
||||
key: 'goodreads',
|
||||
label: 'Goodreads',
|
||||
url: `https://www.goodreads.com/book/show/${ids.goodreads}`,
|
||||
iconUrl: providerIconPath('goodreads'),
|
||||
fallback: 'GR',
|
||||
})
|
||||
}
|
||||
if (ids.amazon) {
|
||||
out.push({
|
||||
key: 'amazon',
|
||||
label: 'Amazon',
|
||||
url: `https://www.amazon.com/dp/${ids.amazon}`,
|
||||
iconUrl: providerIconPath('amazon'),
|
||||
fallback: 'A',
|
||||
})
|
||||
}
|
||||
if (ids.hardcover) {
|
||||
out.push({
|
||||
key: 'hardcover',
|
||||
label: 'Hardcover',
|
||||
url: `https://hardcover.app/books/${ids.hardcover}`,
|
||||
iconUrl: providerIconPath('hardcover'),
|
||||
fallback: 'H',
|
||||
})
|
||||
}
|
||||
if (ids.openLibrary) {
|
||||
const path = String(ids.openLibrary).startsWith('/works/') ? String(ids.openLibrary) : `/works/${ids.openLibrary}`
|
||||
out.push({
|
||||
key: 'openLibrary',
|
||||
label: 'Open Library',
|
||||
url: `https://openlibrary.org${path}`,
|
||||
iconUrl: providerIconPath('openLibrary'),
|
||||
fallback: 'OL',
|
||||
})
|
||||
}
|
||||
if (ids.itunes) {
|
||||
out.push({
|
||||
key: 'itunes',
|
||||
label: 'Apple Books',
|
||||
url: `https://books.apple.com/book/id${ids.itunes}`,
|
||||
iconUrl: providerIconPath('itunes'),
|
||||
fallback: '',
|
||||
})
|
||||
}
|
||||
if (ids.audible) {
|
||||
out.push({
|
||||
key: 'audible',
|
||||
label: 'Audible',
|
||||
url: `https://www.audible.com/pd/${ids.audible}`,
|
||||
iconUrl: providerIconPath('audible'),
|
||||
fallback: 'Au',
|
||||
})
|
||||
}
|
||||
if (ids.librofm) {
|
||||
out.push({
|
||||
key: 'librofm',
|
||||
label: 'Libro.fm',
|
||||
url: libroFmAudiobookUrl(ids.librofm),
|
||||
iconUrl: providerIconPath('librofm'),
|
||||
fallback: 'Lf',
|
||||
})
|
||||
}
|
||||
if (ids.kobo) {
|
||||
out.push({
|
||||
key: 'kobo',
|
||||
label: 'Kobo',
|
||||
url: `https://www.kobo.com/us/en/ebook/${encodeURIComponent(ids.kobo)}`,
|
||||
iconUrl: providerIconPath('kobo'),
|
||||
fallback: 'K',
|
||||
})
|
||||
}
|
||||
if (ids.ranobedb) {
|
||||
out.push({
|
||||
key: 'ranobedb',
|
||||
label: 'RanobeDB',
|
||||
url: `https://ranobedb.org/book/${ids.ranobedb}`,
|
||||
iconUrl: providerIconPath('ranobedb'),
|
||||
fallback: 'RN',
|
||||
})
|
||||
}
|
||||
if (ids.lubimyczytac) {
|
||||
out.push({
|
||||
key: 'lubimyczytac',
|
||||
label: 'LubimyCzytac',
|
||||
url: lubimyczytacBookUrl(ids.lubimyczytac),
|
||||
iconUrl: providerIconPath('lubimyczytac'),
|
||||
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
|
||||
})
|
||||
const providerLinks = computed(() => createBookProviderLinks(props.book.providerIds))
|
||||
|
||||
const communityRatingBadges = computed(() => {
|
||||
const linkByKey = new Map(providerLinks.value.map((link) => [link.key, link]))
|
||||
@@ -2023,7 +1902,7 @@ watch(
|
||||
>
|
||||
<span class="flex size-7 items-center justify-center">
|
||||
<img
|
||||
v-if="!providerIconErrors[link.key]"
|
||||
v-if="link.iconUrl && !providerIconErrors[link.key]"
|
||||
:src="link.iconUrl"
|
||||
:alt="link.label"
|
||||
class="size-4 rounded-[2px] object-contain"
|
||||
|
||||
@@ -1,7 +1,144 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { libroFmAudiobookUrl } from '../provider-links'
|
||||
import { MetadataProviderKey, type ProviderIds } from '@bookorbit/types'
|
||||
import { comicVineIssueUrl, createBookProviderLinks, libroFmAudiobookUrl } from '../provider-links'
|
||||
|
||||
describe('book provider links', () => {
|
||||
it('builds every supported book provider link from one ordered registry', () => {
|
||||
const providerIds: ProviderIds = {
|
||||
google: 'google-id',
|
||||
goodreads: 'goodreads-id',
|
||||
amazon: 'amazon-id',
|
||||
hardcover: 'hardcover-slug',
|
||||
openLibrary: '/works/OL123W',
|
||||
itunes: '123456',
|
||||
audible: 'B012345',
|
||||
librofm: '9781234567890',
|
||||
kobo: 'kobo-slug',
|
||||
comicvine: '1126983',
|
||||
ranobedb: '1287',
|
||||
lubimyczytac: '123/title name',
|
||||
aladin: '456789',
|
||||
}
|
||||
|
||||
expect(createBookProviderLinks(providerIds)).toEqual([
|
||||
{
|
||||
key: MetadataProviderKey.GOOGLE,
|
||||
label: 'Google Books',
|
||||
url: 'https://books.google.com/books?id=google-id',
|
||||
iconUrl: '/assets/provider-icons/google.svg',
|
||||
fallback: 'G',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.GOODREADS,
|
||||
label: 'Goodreads',
|
||||
url: 'https://www.goodreads.com/book/show/goodreads-id',
|
||||
iconUrl: '/assets/provider-icons/goodreads.svg',
|
||||
fallback: 'GR',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.AMAZON,
|
||||
label: 'Amazon',
|
||||
url: 'https://www.amazon.com/dp/amazon-id',
|
||||
iconUrl: '/assets/provider-icons/amazon.svg',
|
||||
fallback: 'A',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.HARDCOVER,
|
||||
label: 'Hardcover',
|
||||
url: 'https://hardcover.app/books/hardcover-slug',
|
||||
iconUrl: '/assets/provider-icons/hardcover.svg',
|
||||
fallback: 'H',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.OPEN_LIBRARY,
|
||||
label: 'Open Library',
|
||||
url: 'https://openlibrary.org/works/OL123W',
|
||||
iconUrl: '/assets/provider-icons/openlibrary.svg',
|
||||
fallback: 'OL',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.ITUNES,
|
||||
label: 'Apple Books',
|
||||
url: 'https://books.apple.com/book/id123456',
|
||||
iconUrl: '/assets/provider-icons/apple-books.svg',
|
||||
fallback: '',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.AUDIBLE,
|
||||
label: 'Audible',
|
||||
url: 'https://www.audible.com/pd/B012345',
|
||||
iconUrl: '/assets/provider-icons/audible.svg',
|
||||
fallback: 'Au',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.LIBROFM,
|
||||
label: 'Libro.fm',
|
||||
url: 'https://libro.fm/audiobooks/9781234567890',
|
||||
iconUrl: '/assets/provider-icons/librofm.svg',
|
||||
fallback: 'Lf',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.KOBO,
|
||||
label: 'Kobo',
|
||||
url: 'https://www.kobo.com/us/en/ebook/kobo-slug',
|
||||
iconUrl: '/assets/provider-icons/kobo.svg',
|
||||
fallback: 'K',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.COMICVINE,
|
||||
label: 'ComicVine',
|
||||
url: 'https://comicvine.gamespot.com/issue/4000-1126983/',
|
||||
iconUrl: null,
|
||||
fallback: 'CV',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.RANOBEDB,
|
||||
label: 'RanobeDB',
|
||||
url: 'https://ranobedb.org/book/1287',
|
||||
iconUrl: '/assets/provider-icons/ranobedb.svg',
|
||||
fallback: 'RN',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.LUBIMYCZYTAC,
|
||||
label: 'LubimyCzytac',
|
||||
url: 'https://lubimyczytac.pl/ksiazka/123/title%20name',
|
||||
iconUrl: '/assets/provider-icons/lubimyczytac.svg',
|
||||
fallback: 'LC',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.ALADIN,
|
||||
label: 'Aladin',
|
||||
url: 'https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=456789',
|
||||
iconUrl: '/assets/provider-icons/aladin.svg',
|
||||
fallback: '알',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('trims provider IDs, safely encodes URL values, and ignores empty IDs', () => {
|
||||
const links = createBookProviderLinks({
|
||||
google: ' id with spaces&extra=true ',
|
||||
comicvine: ' 1126983 ',
|
||||
goodreads: ' ',
|
||||
amazon: null,
|
||||
})
|
||||
|
||||
expect(links).toEqual([
|
||||
expect.objectContaining({
|
||||
key: MetadataProviderKey.GOOGLE,
|
||||
url: 'https://books.google.com/books?id=id%20with%20spaces%26extra%3Dtrue',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
key: MetadataProviderKey.COMICVINE,
|
||||
url: 'https://comicvine.gamespot.com/issue/4000-1126983/',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('constructs the generic ComicVine issue URL from a bare issue ID', () => {
|
||||
expect(comicVineIssueUrl(' 1126983 ')).toBe('https://comicvine.gamespot.com/issue/4000-1126983/')
|
||||
})
|
||||
|
||||
describe('provider links', () => {
|
||||
it('builds a trimmed and encoded Libro.fm audiobook URL', () => {
|
||||
expect(libroFmAudiobookUrl(' 978 123 ')).toBe('https://libro.fm/audiobooks/978%20123')
|
||||
})
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
import { MetadataProviderKey, type ProviderIds } from '@bookorbit/types'
|
||||
import { providerIconPathSafe } from './provider-icons'
|
||||
|
||||
export type BookProviderLink = {
|
||||
key: MetadataProviderKey
|
||||
label: string
|
||||
url: string
|
||||
iconUrl: string | null
|
||||
fallback: string
|
||||
}
|
||||
|
||||
type ProviderLinkDefinition = Omit<BookProviderLink, 'url' | 'iconUrl'> & {
|
||||
url: (id: string) => string
|
||||
}
|
||||
|
||||
function encoded(value: string): string {
|
||||
return encodeURIComponent(value)
|
||||
}
|
||||
|
||||
export function lubimyczytacBookUrl(id: string): string {
|
||||
const value = String(id).trim()
|
||||
// New data stores the canonical "id/slug" path. lubimyczytac.pl 404s on a slug-less id,
|
||||
@@ -9,3 +28,111 @@ export function lubimyczytacBookUrl(id: string): string {
|
||||
export function libroFmAudiobookUrl(id: string): string {
|
||||
return `https://libro.fm/audiobooks/${encodeURIComponent(String(id).trim())}`
|
||||
}
|
||||
|
||||
export function comicVineIssueUrl(id: string): string {
|
||||
return `https://comicvine.gamespot.com/issue/4000-${encoded(String(id).trim())}/`
|
||||
}
|
||||
|
||||
function openLibraryBookUrl(id: string): string {
|
||||
const value = String(id).trim()
|
||||
const workId = value.startsWith('/works/') ? value.slice('/works/'.length) : value
|
||||
return `https://openlibrary.org/works/${encoded(workId)}`
|
||||
}
|
||||
|
||||
const PROVIDER_LINK_DEFINITIONS: readonly ProviderLinkDefinition[] = [
|
||||
{
|
||||
key: MetadataProviderKey.GOOGLE,
|
||||
label: 'Google Books',
|
||||
url: (id) => `https://books.google.com/books?id=${encoded(id)}`,
|
||||
fallback: 'G',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.GOODREADS,
|
||||
label: 'Goodreads',
|
||||
url: (id) => `https://www.goodreads.com/book/show/${encoded(id)}`,
|
||||
fallback: 'GR',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.AMAZON,
|
||||
label: 'Amazon',
|
||||
url: (id) => `https://www.amazon.com/dp/${encoded(id)}`,
|
||||
fallback: 'A',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.HARDCOVER,
|
||||
label: 'Hardcover',
|
||||
url: (id) => `https://hardcover.app/books/${encoded(id)}`,
|
||||
fallback: 'H',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.OPEN_LIBRARY,
|
||||
label: 'Open Library',
|
||||
url: openLibraryBookUrl,
|
||||
fallback: 'OL',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.ITUNES,
|
||||
label: 'Apple Books',
|
||||
url: (id) => `https://books.apple.com/book/id${encoded(id)}`,
|
||||
fallback: '',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.AUDIBLE,
|
||||
label: 'Audible',
|
||||
url: (id) => `https://www.audible.com/pd/${encoded(id)}`,
|
||||
fallback: 'Au',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.LIBROFM,
|
||||
label: 'Libro.fm',
|
||||
url: libroFmAudiobookUrl,
|
||||
fallback: 'Lf',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.KOBO,
|
||||
label: 'Kobo',
|
||||
url: (id) => `https://www.kobo.com/us/en/ebook/${encoded(id)}`,
|
||||
fallback: 'K',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.COMICVINE,
|
||||
label: 'ComicVine',
|
||||
url: comicVineIssueUrl,
|
||||
fallback: 'CV',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.RANOBEDB,
|
||||
label: 'RanobeDB',
|
||||
url: (id) => `https://ranobedb.org/book/${encoded(id)}`,
|
||||
fallback: 'RN',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.LUBIMYCZYTAC,
|
||||
label: 'LubimyCzytac',
|
||||
url: lubimyczytacBookUrl,
|
||||
fallback: 'LC',
|
||||
},
|
||||
{
|
||||
key: MetadataProviderKey.ALADIN,
|
||||
label: 'Aladin',
|
||||
url: (id) => `https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=${encoded(id)}`,
|
||||
fallback: '알',
|
||||
},
|
||||
]
|
||||
|
||||
export function createBookProviderLinks(providerIds: ProviderIds): BookProviderLink[] {
|
||||
return PROVIDER_LINK_DEFINITIONS.flatMap((definition) => {
|
||||
const id = providerIds[definition.key]?.trim()
|
||||
if (!id) return []
|
||||
|
||||
return [
|
||||
{
|
||||
key: definition.key,
|
||||
label: definition.label,
|
||||
url: definition.url(id),
|
||||
iconUrl: providerIconPathSafe(definition.key),
|
||||
fallback: definition.fallback,
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user