Initial commit: Shelfless – alternative Audiobookshelf frontend
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>
This commit is contained in:
334
src/types/abs.ts
Normal file
334
src/types/abs.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user