React + Vite + TypeScript SPA covering the full ABS feature set (library browsing, item detail, metadata/cover editing, podcasts, player with session sync, admin: users/libraries/scanner/server settings). Dev uses a dynamic CORS proxy; production is served by server/index.mjs (static + reverse proxy to ABS_URL). Includes systemd unit and installer under deploy/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
335 lines
7.9 KiB
TypeScript
335 lines
7.9 KiB
TypeScript
/**
|
|
* Audiobookshelf API types. Only fields Shelfless uses are typed; unknown extras are
|
|
* tolerated. See memory: abs-api-corrections.
|
|
*
|
|
* ABS returns two shapes for media:
|
|
* - "minified" (library item lists): metadata uses flattened strings
|
|
* (authorName, narratorName, seriesName).
|
|
* - "expanded" (single item with ?expanded=1): metadata uses arrays of objects
|
|
* (authors[], narrators[], series[]).
|
|
* We model both with optional fields and read defensively via helpers in lib/media.
|
|
*/
|
|
|
|
// ── Auth ──────────────────────────────────────────────────────────────────
|
|
export type UserType = 'root' | 'admin' | 'user' | 'guest'
|
|
|
|
export interface UserPermissions {
|
|
download: boolean
|
|
update: boolean
|
|
delete: boolean
|
|
upload: boolean
|
|
accessAllLibraries: boolean
|
|
accessAllTags: boolean
|
|
accessExplicitContent: boolean
|
|
[key: string]: boolean | undefined
|
|
}
|
|
|
|
export interface AbsUser {
|
|
id: string
|
|
username: string
|
|
email?: string | null
|
|
type: UserType
|
|
token: string
|
|
isActive?: boolean
|
|
isLocked?: boolean
|
|
lastSeen?: number | null
|
|
createdAt?: number
|
|
permissions: UserPermissions
|
|
librariesAccessible?: string[]
|
|
itemTagsAccessible?: string[]
|
|
}
|
|
|
|
export interface LoginResponse {
|
|
user: AbsUser
|
|
userDefaultLibraryId?: string
|
|
serverSettings?: Record<string, unknown>
|
|
source?: string
|
|
}
|
|
|
|
export interface AuthorizeResponse {
|
|
user: AbsUser
|
|
userDefaultLibraryId?: string
|
|
}
|
|
|
|
export function isAdminUser(user: Pick<AbsUser, 'type'> | null | undefined): boolean {
|
|
return user?.type === 'root' || user?.type === 'admin'
|
|
}
|
|
|
|
// ── Libraries ───────────────────────────────────────────────────────────────
|
|
export type MediaType = 'book' | 'podcast'
|
|
|
|
export interface LibraryFolder {
|
|
id: string
|
|
fullPath: string
|
|
libraryId?: string
|
|
addedAt?: number
|
|
}
|
|
|
|
export interface LibrarySettings {
|
|
coverAspectRatio?: number
|
|
disableWatcher?: boolean
|
|
autoScanCronExpression?: string | null
|
|
[key: string]: unknown
|
|
}
|
|
|
|
export interface Library {
|
|
id: string
|
|
name: string
|
|
folders: LibraryFolder[]
|
|
displayOrder: number
|
|
icon: string
|
|
mediaType: MediaType
|
|
provider: string
|
|
settings?: LibrarySettings
|
|
createdAt: number
|
|
lastUpdate: number
|
|
}
|
|
|
|
export interface LibrariesResponse {
|
|
libraries: Library[]
|
|
}
|
|
|
|
export interface LibraryStats {
|
|
totalItems?: number
|
|
totalAuthors?: number
|
|
totalGenres?: number
|
|
totalDuration?: number
|
|
totalSize?: number
|
|
numAudioTracks?: number
|
|
[key: string]: unknown
|
|
}
|
|
|
|
// ── Metadata / Media ─────────────────────────────────────────────────────────
|
|
export interface AuthorRef {
|
|
id?: string
|
|
name: string
|
|
}
|
|
export interface SeriesRef {
|
|
id?: string
|
|
name: string
|
|
sequence?: string | null
|
|
}
|
|
|
|
export interface BookMetadata {
|
|
title: string | null
|
|
titleIgnorePrefix?: string
|
|
subtitle?: string | null
|
|
// expanded
|
|
authors?: AuthorRef[]
|
|
narrators?: string[]
|
|
series?: SeriesRef[]
|
|
// minified
|
|
authorName?: string
|
|
narratorName?: string
|
|
seriesName?: string
|
|
genres?: string[]
|
|
publishedYear?: string | null
|
|
publishedDate?: string | null
|
|
publisher?: string | null
|
|
description?: string | null
|
|
isbn?: string | null
|
|
asin?: string | null
|
|
language?: string | null
|
|
explicit?: boolean
|
|
}
|
|
|
|
export interface Chapter {
|
|
id: number
|
|
start: number
|
|
end: number
|
|
title: string
|
|
}
|
|
|
|
export interface AudioFileMeta {
|
|
filename: string
|
|
ext: string
|
|
path: string
|
|
relPath?: string
|
|
size: number
|
|
}
|
|
|
|
export interface AudioFile {
|
|
index: number
|
|
ino: string
|
|
metadata: AudioFileMeta
|
|
duration: number
|
|
bitRate?: number
|
|
codec?: string
|
|
channels?: number
|
|
mimeType?: string
|
|
}
|
|
|
|
export interface Book {
|
|
id?: string
|
|
libraryItemId?: string
|
|
metadata: BookMetadata
|
|
coverPath: string | null
|
|
tags?: string[]
|
|
audioFiles?: AudioFile[]
|
|
chapters?: Chapter[]
|
|
duration?: number
|
|
size?: number
|
|
numTracks?: number
|
|
numChapters?: number
|
|
numAudioFiles?: number
|
|
ebookFormat?: string | null
|
|
}
|
|
|
|
export interface PodcastMetadata {
|
|
title: string | null
|
|
author?: string | null
|
|
description?: string | null
|
|
releaseDate?: string | null
|
|
genres?: string[]
|
|
feedUrl?: string | null
|
|
imageUrl?: string | null
|
|
itunesId?: string | null
|
|
itunesArtistId?: string | null
|
|
language?: string | null
|
|
explicit?: boolean
|
|
type?: string | null
|
|
}
|
|
|
|
export interface PodcastEpisode {
|
|
id: string
|
|
index?: number
|
|
episode?: string | null
|
|
season?: string | null
|
|
title: string
|
|
subtitle?: string | null
|
|
description?: string | null
|
|
pubDate?: string | null
|
|
publishedAt?: number | null
|
|
audioFile?: AudioFile
|
|
duration?: number
|
|
size?: number
|
|
}
|
|
|
|
export interface Podcast {
|
|
id?: string
|
|
libraryItemId?: string
|
|
metadata: PodcastMetadata
|
|
coverPath: string | null
|
|
tags?: string[]
|
|
episodes?: PodcastEpisode[]
|
|
numEpisodes?: number
|
|
autoDownloadEpisodes?: boolean
|
|
}
|
|
|
|
export type Media = Book | Podcast
|
|
|
|
// ── Library items ─────────────────────────────────────────────────────────
|
|
export interface LibraryFile {
|
|
ino: string
|
|
metadata: AudioFileMeta
|
|
fileType?: string
|
|
addedAt?: number
|
|
updatedAt?: number
|
|
}
|
|
|
|
export interface LibraryItem {
|
|
id: string
|
|
ino?: string
|
|
libraryId: string
|
|
folderId?: string
|
|
path?: string
|
|
relPath?: string
|
|
isFile?: boolean
|
|
mtimeMs?: number
|
|
ctimeMs?: number
|
|
birthtimeMs?: number
|
|
addedAt: number
|
|
updatedAt: number
|
|
isMissing?: boolean
|
|
isInvalid?: boolean
|
|
mediaType: MediaType
|
|
media: Media
|
|
libraryFiles?: LibraryFile[]
|
|
numFiles?: number
|
|
size?: number
|
|
// present on some list/expanded responses
|
|
collapsedSeries?: unknown
|
|
recentEpisode?: PodcastEpisode
|
|
progress?: MediaProgress
|
|
}
|
|
|
|
export interface LibraryItemsResponse {
|
|
results: LibraryItem[]
|
|
total: number
|
|
limit: number
|
|
page: number
|
|
sortBy?: string
|
|
sortDesc?: boolean
|
|
mediaType?: MediaType
|
|
minified?: boolean
|
|
}
|
|
|
|
export interface LibrarySearchResult {
|
|
book?: { libraryItem: LibraryItem; matchKey?: string; matchText?: string }[]
|
|
podcast?: { libraryItem: LibraryItem }[]
|
|
authors?: { id: string; name: string }[]
|
|
series?: { series: SeriesRef; books: LibraryItem[] }[]
|
|
tags?: string[]
|
|
}
|
|
|
|
// ── Progress & sessions ─────────────────────────────────────────────────────
|
|
export interface MediaProgress {
|
|
id: string
|
|
libraryItemId: string
|
|
episodeId?: string | null
|
|
duration: number
|
|
progress: number // 0..1
|
|
currentTime: number
|
|
isFinished: boolean
|
|
hideFromContinueListening?: boolean
|
|
lastUpdate?: number
|
|
startedAt?: number
|
|
finishedAt?: number | null
|
|
}
|
|
|
|
export interface ItemsInProgressResponse {
|
|
libraryItems: LibraryItem[]
|
|
}
|
|
|
|
export interface AudioTrack {
|
|
index: number
|
|
startOffset: number
|
|
duration: number
|
|
title?: string
|
|
contentUrl: string
|
|
mimeType: string
|
|
codec?: string
|
|
metadata?: AudioFileMeta
|
|
}
|
|
|
|
export interface PlaybackSession {
|
|
id: string
|
|
userId: string
|
|
libraryId?: string
|
|
libraryItemId: string
|
|
episodeId?: string | null
|
|
mediaType: MediaType
|
|
displayTitle?: string
|
|
displayAuthor?: string
|
|
coverPath?: string | null
|
|
duration: number
|
|
playMethod?: number
|
|
mediaPlayer?: string
|
|
audioTracks: AudioTrack[]
|
|
chapters?: Chapter[]
|
|
currentTime: number
|
|
startedAt?: number
|
|
updatedAt?: number
|
|
}
|
|
|
|
// ── Server / admin ──────────────────────────────────────────────────────────
|
|
export interface BackupInfo {
|
|
id: string
|
|
backupMetadataCovers?: boolean
|
|
fileName: string
|
|
path?: string
|
|
serverVersion?: string
|
|
createdAt: number
|
|
size?: number
|
|
}
|