← Public packages
@kentcdodds/audible
Personal Audible library app — browser re-auth, library, wishlist, NAS archive stub.
src/library.ts
160 lines · 4.8 KB · TypeScriptimport { createClient } from './client.ts'
import { classifyArchiveEligibility } from './archive-eligibility.ts'
import type { SlimLibraryItem } from './types.ts'
const DEFAULT_RESPONSE_GROUPS = [
'product_desc',
'product_attrs',
'contributors',
'media',
'series',
'percent_complete',
'is_finished',
'product_details',
// Required for reliable is_ayce / benefit_id on library *list* (Plus catalog).
'customer_rights',
].join(',')
function pickCoverUrl(product: Record<string, unknown>): string | undefined {
const images = product.product_images
if (images && typeof images === 'object' && !Array.isArray(images)) {
const map = images as Record<string, unknown>
for (const key of ['500', '300', '200', '100', '60']) {
const v = map[key]
if (typeof v === 'string' && v.startsWith('http')) return v
}
for (const v of Object.values(map)) {
if (typeof v === 'string' && v.startsWith('http')) return v
}
}
if (typeof product.cover_url === 'string') return product.cover_url
return undefined
}
function slimItem(raw: Record<string, unknown>): SlimLibraryItem {
const product = (raw.item ?? raw) as Record<string, unknown>
const authors = Array.isArray(product.authors)
? (product.authors as Array<{ name?: string }>)
.map((a) => a.name)
.filter((n): n is string => Boolean(n))
: undefined
const narrators = Array.isArray(product.narrators)
? (product.narrators as Array<{ name?: string }>)
.map((a) => a.name)
.filter((n): n is string => Boolean(n))
: undefined
const series = Array.isArray(product.series)
? (product.series as Array<{ title?: string; sequence?: string }>).map(
(s) => ({ title: s.title, sequence: s.sequence }),
)
: undefined
const eligibility = classifyArchiveEligibility(product)
const benefitId =
typeof product.benefit_id === 'string'
? product.benefit_id
: product.benefit_id === null
? null
: undefined
return {
asin: String(product.asin ?? ''),
title:
typeof product.title === 'string'
? product.title
: typeof product.raw_title === 'string'
? product.raw_title
: undefined,
authors,
narrators,
purchaseDate:
typeof product.purchase_date === 'string'
? product.purchase_date
: undefined,
runtimeLengthMin:
typeof product.runtime_length_min === 'number'
? product.runtime_length_min
: undefined,
isFinished:
typeof product.is_finished === 'boolean' ? product.is_finished : undefined,
percentComplete:
typeof product.percent_complete === 'number'
? product.percent_complete
: undefined,
series,
coverUrl: pickCoverUrl(product),
archiveEligibility: eligibility.archiveEligibility,
archiveHint: eligibility.archiveHint,
isAyce: product.is_ayce === true,
benefitId,
contentType:
typeof product.content_type === 'string' ? product.content_type : undefined,
contentDeliveryType:
typeof product.content_delivery_type === 'string'
? product.content_delivery_type
: undefined,
formatType:
typeof product.format_type === 'string' ? product.format_type : undefined,
}
}
/**
* List or search titles in the signed-in Audible library (GET /1.0/library).
* Use for personal archive inventory of owned titles.
*
* @param input - Optional search filters, pagination, and slim vs raw shape
* @returns Library page with slim items by default
*
* @example
* import listLibrary from 'kody:@kentcdodds/audible/library'
*
* const page = await listLibrary({ title: 'Project Hail Mary', numResults: 20 })
*/
export default async function listLibrary(input: {
title?: string
author?: string
numResults?: number
page?: number
purchasedAfter?: string
sortBy?: string
status?: 'Active' | 'Revoked'
responseGroups?: string
/** When true, return the raw Audible payload instead of slim items. */
raw?: boolean
locale?: string
} = {}) {
const client = await createClient({ locale: input.locale })
const numResults = Math.min(Math.max(input.numResults ?? 50, 1), 1000)
const payload = await client.request<{
items?: Array<Record<string, unknown>>
total_results?: number
}>({
path: '/1.0/library',
query: {
num_results: numResults,
page: input.page ?? 1,
title: input.title,
author: input.author,
purchased_after: input.purchasedAfter,
sort_by: input.sortBy ?? '-PurchaseDate',
status: input.status ?? 'Active',
response_groups: input.responseGroups ?? DEFAULT_RESPONSE_GROUPS,
},
})
if (input.raw) {
return {
locale: client.locale.countryCode,
apiHost: client.locale.apiHost,
totalResults: payload.total_results ?? payload.items?.length ?? 0,
items: payload.items ?? [],
}
}
const items = (payload.items ?? []).map(slimItem).filter((i) => i.asin)
return {
locale: client.locale.countryCode,
apiHost: client.locale.apiHost,
totalResults: payload.total_results ?? items.length,
count: items.length,
items,
}
}