mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
* feat: add filtering and sorting across library, explore, and upcoming Add comprehensive filtering and sorting to all list views on both web and mobile. Library (new dedicated page): - New /library route (web) with URL search param persistence - New Library tab (5th tab, mobile) with FlashList grid - Filters: status (multi-select), type, genre (library-only), user rating range, release year (decade presets + custom), content rating, available to stream - Sort by: title, date added, release date, popularity, user rating, TMDB rating - Text search within collection - Filter popover (web) / bottom sheet with Apply (mobile) - Remove old dashboard.library endpoint in favor of library.list Explore/Discover: - New Discover section on Explore page with inline filter controls - TMDB filters: genre (now optional), year range, min rating, sort order, original language, streaming provider - Watch provider list endpoint (WATCH_REGION env var, default "US") - Default to popular content when no filters selected Upcoming: - Type filter (All/Movies/TV Shows) and status filter (All/Watching/Watchlist) - Toggle chips on both web (URL params) and mobile - Backend: mediaType and statusFilter params on upcoming endpoint Navigation: - Library added to web nav bar (desktop + mobile tab bar) - Library added as 5th tab on mobile (between Home and Explore) - Dashboard library section simplified to compact preview with "See all" link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add user streaming platform subscriptions and redesign filter UI Replace the denormalized availabilityOffers table with a normalized platforms + titleAvailability + userPlatforms schema. Users can now declare which streaming services they subscribe to, enabling personalized "On your services" availability display and filtered library/explore results. Backend: new platforms table seeded from TMDB provider data on startup, new API endpoints (platforms.list, account.platforms, account.updatePlatforms), title detail annotates availability with isUserSubscribed flag, library "on my services" filter checks user's subscribed platforms. Web UI: post-registration onboarding page for platform selection, streaming services settings section, title detail split into "On your services" / "Also available on", explore dropdown uses platforms table with active filter highlighting. Library filters redesigned from cluttered popover to clean collapsible inline strip with consolidated dropdowns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address race conditions, stale state, and edge cases in platforms feature - Fix debounced autosave race in streaming services settings: use ref for latest selection + counter guard so only the most recent save updates UI. Clean up timers on unmount. - Fix explore provider dropdown desyncing from query filter when switching Movie/TV type: reset selectedPlatformId alongside providerId. - Fix platformIdsExist rejecting duplicate valid IDs by deduplicating input before comparing against DB results. - Fix stale platform metadata: always upsert name/logo on TMDB refresh instead of skipping when platform already exists. - Fix invisible ratingMax filter: clear ratingMax when user changes ratingMin since the UI only exposes a single rating dropdown now. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clear stale saved-state timer and preserve falsy filter values - Clear previous savedTimerRef timeout before scheduling a new one in streaming-services-section, preventing an older timer from hiding the "Saved" indicator too early on rapid successive saves - Replace `value || undefined` with explicit empty-value check in library filter handler so numeric 0 (e.g. ratingMin=0) is not incorrectly dropped Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add TanStack Form and migrate auth + change password forms Set up TanStack Form v1 composition layer with createFormHook, pre-bound field components (TextField, TextareaField, CheckboxField, SwitchField, SubmitButton), and migrate the two traditional submit-based forms: - AuthForm: replace 4 useState calls with useAppForm, use form.Field for each input while preserving motion animation layout - ChangePasswordDialog: replace 6 useState calls with useAppForm + Zod schema validation (cross-field password match, min length), per-field error display, form.reset() on dialog close Also adds @tanstack/react-form to the monorepo catalog and updates both web and native apps to reference it via catalog:. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8457 lines
260 KiB
JSON
8457 lines
260 KiB
JSON
{
|
|
"info": {
|
|
"title": "Sofa API",
|
|
"version": "0.1.1"
|
|
},
|
|
"servers": [
|
|
{
|
|
"url": "{protocol}://{host}:{port}/api/v1",
|
|
"variables": {
|
|
"protocol": {
|
|
"default": "http",
|
|
"enum": [
|
|
"http",
|
|
"https"
|
|
]
|
|
},
|
|
"host": {
|
|
"default": "localhost"
|
|
},
|
|
"port": {
|
|
"default": "3000"
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"tags": [
|
|
{
|
|
"name": "Titles",
|
|
"description": "Movie and TV show management"
|
|
},
|
|
{
|
|
"name": "Episodes",
|
|
"description": "Episode watch tracking"
|
|
},
|
|
{
|
|
"name": "Seasons",
|
|
"description": "Season watch tracking"
|
|
},
|
|
{
|
|
"name": "People",
|
|
"description": "Cast and crew information"
|
|
},
|
|
{
|
|
"name": "Dashboard",
|
|
"description": "User dashboard data"
|
|
},
|
|
{
|
|
"name": "Explore",
|
|
"description": "Discover trending and popular content"
|
|
},
|
|
{
|
|
"name": "Search",
|
|
"description": "Search for movies and TV shows"
|
|
},
|
|
{
|
|
"name": "Discover",
|
|
"description": "Advanced content discovery with filters"
|
|
},
|
|
{
|
|
"name": "System",
|
|
"description": "Server status and configuration"
|
|
},
|
|
{
|
|
"name": "Integrations",
|
|
"description": "Media server integrations"
|
|
},
|
|
{
|
|
"name": "Admin",
|
|
"description": "Server administration"
|
|
},
|
|
{
|
|
"name": "Account",
|
|
"description": "User account management"
|
|
}
|
|
],
|
|
"components": {
|
|
"securitySchemes": {
|
|
"session": {
|
|
"type": "apiKey",
|
|
"name": "better-auth.session_token",
|
|
"in": "cookie",
|
|
"description": "Better Auth session cookie"
|
|
}
|
|
},
|
|
"schemas": {
|
|
"Title": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Internal UUIDv7 identifier"
|
|
},
|
|
"tmdbId": {
|
|
"type": "number",
|
|
"description": "TMDB numeric ID"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Display title (localized)"
|
|
},
|
|
"originalTitle": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Original language title"
|
|
},
|
|
"overview": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Plot synopsis"
|
|
},
|
|
"releaseDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Theatrical release date (movies, ISO 8601)"
|
|
},
|
|
"firstAirDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "First air date (TV shows, ISO 8601)"
|
|
},
|
|
"posterPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Poster image path"
|
|
},
|
|
"posterThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the poster"
|
|
},
|
|
"backdropPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Backdrop image path"
|
|
},
|
|
"backdropThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the backdrop"
|
|
},
|
|
"popularity": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "TMDB popularity score"
|
|
},
|
|
"voteAverage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Average user rating (0-10)"
|
|
},
|
|
"voteCount": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Total number of votes"
|
|
},
|
|
"status": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Production status (e.g. Released, Returning Series)"
|
|
},
|
|
"contentRating": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Content rating (e.g. PG-13, TV-MA)"
|
|
},
|
|
"imdbId": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "IMDb title ID (e.g. tt0137523)"
|
|
},
|
|
"tvdbId": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "TVDB ID (TV shows only)"
|
|
},
|
|
"originalLanguage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Original language ISO 639-1 code (e.g. en)"
|
|
},
|
|
"runtimeMinutes": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Runtime in minutes (movies only)"
|
|
},
|
|
"colorPalette": {
|
|
"anyOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"vibrant": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"darkVibrant": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"lightVibrant": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"muted": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"darkMuted": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"lightMuted": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"vibrant",
|
|
"darkVibrant",
|
|
"lightVibrant",
|
|
"muted",
|
|
"darkMuted",
|
|
"lightMuted"
|
|
],
|
|
"description": "Extracted color palette from the title poster image (CSS hex values)"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"trailerVideoKey": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "YouTube video key for the trailer"
|
|
},
|
|
"genres": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"description": "Genre names"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"tmdbId",
|
|
"type",
|
|
"title",
|
|
"originalTitle",
|
|
"overview",
|
|
"releaseDate",
|
|
"firstAirDate",
|
|
"posterPath",
|
|
"posterThumbHash",
|
|
"backdropPath",
|
|
"backdropThumbHash",
|
|
"popularity",
|
|
"voteAverage",
|
|
"voteCount",
|
|
"status",
|
|
"contentRating",
|
|
"imdbId",
|
|
"tvdbId",
|
|
"originalLanguage",
|
|
"runtimeMinutes",
|
|
"colorPalette",
|
|
"trailerVideoKey",
|
|
"genres"
|
|
],
|
|
"description": "A fully resolved movie or TV show from TMDB"
|
|
},
|
|
"Person": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Internal UUIDv7 identifier"
|
|
},
|
|
"tmdbId": {
|
|
"type": "number",
|
|
"description": "TMDB person ID"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Full name"
|
|
},
|
|
"biography": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Biography text"
|
|
},
|
|
"birthday": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Date of birth (ISO 8601)"
|
|
},
|
|
"deathday": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Date of death (ISO 8601)"
|
|
},
|
|
"placeOfBirth": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Place of birth"
|
|
},
|
|
"profilePath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Profile photo image path"
|
|
},
|
|
"profileThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the profile photo"
|
|
},
|
|
"knownForDepartment": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Primary department (e.g. Acting, Directing)"
|
|
},
|
|
"imdbId": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "IMDb person ID (e.g. nm0000123)"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"tmdbId",
|
|
"name",
|
|
"biography",
|
|
"birthday",
|
|
"deathday",
|
|
"placeOfBirth",
|
|
"profilePath",
|
|
"profileThumbHash",
|
|
"knownForDepartment",
|
|
"imdbId"
|
|
],
|
|
"description": "A person (actor, director, crew member) from TMDB"
|
|
},
|
|
"PersonCredit": {
|
|
"type": "object",
|
|
"properties": {
|
|
"titleId": {
|
|
"type": "string",
|
|
"description": "Internal title ID"
|
|
},
|
|
"tmdbId": {
|
|
"type": "number",
|
|
"description": "TMDB title ID"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Title name"
|
|
},
|
|
"posterPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Poster image path"
|
|
},
|
|
"posterThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the poster"
|
|
},
|
|
"releaseDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Release date (ISO 8601)"
|
|
},
|
|
"firstAirDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "First air date (ISO 8601)"
|
|
},
|
|
"voteAverage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Average rating (0-10)"
|
|
},
|
|
"character": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Character name (for acting credits)"
|
|
},
|
|
"department": {
|
|
"type": "string",
|
|
"description": "Department (e.g. Acting, Directing)"
|
|
},
|
|
"job": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Job title (for crew credits)"
|
|
}
|
|
},
|
|
"required": [
|
|
"titleId",
|
|
"tmdbId",
|
|
"type",
|
|
"title",
|
|
"posterPath",
|
|
"posterThumbHash",
|
|
"releaseDate",
|
|
"firstAirDate",
|
|
"voteAverage",
|
|
"character",
|
|
"department",
|
|
"job"
|
|
],
|
|
"description": "A person's credit in a movie or TV show"
|
|
},
|
|
"Episode": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Episode ID"
|
|
},
|
|
"episodeNumber": {
|
|
"type": "number",
|
|
"description": "Episode number within the season"
|
|
},
|
|
"name": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Episode title"
|
|
},
|
|
"overview": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Episode plot summary"
|
|
},
|
|
"stillPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Episode still image path"
|
|
},
|
|
"stillThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the still image"
|
|
},
|
|
"airDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Original air date (ISO 8601)"
|
|
},
|
|
"runtimeMinutes": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Episode runtime in minutes"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"episodeNumber",
|
|
"name",
|
|
"overview",
|
|
"stillPath",
|
|
"stillThumbHash",
|
|
"airDate",
|
|
"runtimeMinutes"
|
|
],
|
|
"description": "A single TV episode"
|
|
},
|
|
"Season": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Season ID"
|
|
},
|
|
"seasonNumber": {
|
|
"type": "number",
|
|
"description": "Season number (0 for specials)"
|
|
},
|
|
"name": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Season name"
|
|
},
|
|
"episodes": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/Episode"
|
|
},
|
|
"description": "Episodes in this season"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"seasonNumber",
|
|
"name",
|
|
"episodes"
|
|
],
|
|
"description": "A TV season with its episodes"
|
|
},
|
|
"CastMember": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Credit ID"
|
|
},
|
|
"personId": {
|
|
"type": "string",
|
|
"description": "Internal person ID"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Person's name"
|
|
},
|
|
"character": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Character name (for acting credits)"
|
|
},
|
|
"department": {
|
|
"type": "string",
|
|
"description": "Department (e.g. Acting, Directing)"
|
|
},
|
|
"job": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Job title (for crew credits)"
|
|
},
|
|
"displayOrder": {
|
|
"type": "number",
|
|
"description": "Sort order in the credits list"
|
|
},
|
|
"episodeCount": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Number of episodes (TV only)"
|
|
},
|
|
"profilePath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Profile photo image path"
|
|
},
|
|
"profileThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the profile photo"
|
|
},
|
|
"tmdbId": {
|
|
"type": "number",
|
|
"description": "TMDB person ID"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"personId",
|
|
"name",
|
|
"character",
|
|
"department",
|
|
"job",
|
|
"displayOrder",
|
|
"episodeCount",
|
|
"profilePath",
|
|
"profileThumbHash",
|
|
"tmdbId"
|
|
],
|
|
"description": "A cast or crew member credit"
|
|
},
|
|
"BrowseItem": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Internal title ID"
|
|
},
|
|
"tmdbId": {
|
|
"type": "number",
|
|
"description": "TMDB numeric ID"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Display title"
|
|
},
|
|
"posterPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Poster image path"
|
|
},
|
|
"posterThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the poster"
|
|
},
|
|
"releaseDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Release date (ISO 8601)"
|
|
},
|
|
"firstAirDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "First air date (ISO 8601)"
|
|
},
|
|
"voteAverage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Average rating (0-10)"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"tmdbId",
|
|
"type",
|
|
"title",
|
|
"posterPath",
|
|
"posterThumbHash",
|
|
"releaseDate",
|
|
"firstAirDate",
|
|
"voteAverage"
|
|
],
|
|
"description": "A TMDB title card used in browse/trending/popular lists"
|
|
},
|
|
"Recommendation": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Internal title ID"
|
|
},
|
|
"tmdbId": {
|
|
"type": "number",
|
|
"description": "TMDB numeric ID"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Display title"
|
|
},
|
|
"posterPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Poster image path"
|
|
},
|
|
"posterThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the poster"
|
|
},
|
|
"releaseDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Release date (ISO 8601)"
|
|
},
|
|
"firstAirDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "First air date (ISO 8601)"
|
|
},
|
|
"voteAverage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Average rating (0-10)"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"tmdbId",
|
|
"type",
|
|
"title",
|
|
"posterPath",
|
|
"posterThumbHash",
|
|
"releaseDate",
|
|
"firstAirDate",
|
|
"voteAverage"
|
|
],
|
|
"description": "A recommended title"
|
|
},
|
|
"Integration": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Integration ID"
|
|
},
|
|
"provider": {
|
|
"type": "string",
|
|
"description": "Provider name (plex, jellyfin, etc.)"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"webhook",
|
|
"list"
|
|
],
|
|
"type": "string",
|
|
"description": "Integration type: webhook (Plex/Jellyfin/Emby) or list (Sonarr/Radarr)"
|
|
},
|
|
"token": {
|
|
"type": "string",
|
|
"description": "Webhook authentication token"
|
|
},
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Whether the integration is active"
|
|
},
|
|
"lastEventAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Last received event timestamp (ISO 8601)"
|
|
},
|
|
"createdAt": {
|
|
"type": "string",
|
|
"description": "When the integration was created (ISO 8601)"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"provider",
|
|
"type",
|
|
"token",
|
|
"enabled",
|
|
"lastEventAt",
|
|
"createdAt"
|
|
],
|
|
"description": "A media server integration configuration"
|
|
},
|
|
"IntegrationEvent": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Event ID"
|
|
},
|
|
"eventType": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Webhook event type"
|
|
},
|
|
"mediaType": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Media type from the event"
|
|
},
|
|
"mediaTitle": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Title from the event"
|
|
},
|
|
"status": {
|
|
"enum": [
|
|
"success",
|
|
"ignored",
|
|
"error"
|
|
],
|
|
"type": "string",
|
|
"description": "Event processing outcome"
|
|
},
|
|
"receivedAt": {
|
|
"type": "string",
|
|
"description": "When the event was received (ISO 8601)"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"eventType",
|
|
"mediaType",
|
|
"mediaTitle",
|
|
"status",
|
|
"receivedAt"
|
|
],
|
|
"description": "A webhook or sync event from a media server"
|
|
},
|
|
"Backup": {
|
|
"type": "object",
|
|
"properties": {
|
|
"filename": {
|
|
"type": "string",
|
|
"description": "Backup filename on disk"
|
|
},
|
|
"sizeBytes": {
|
|
"type": "number",
|
|
"description": "Backup file size in bytes"
|
|
},
|
|
"createdAt": {
|
|
"type": "string",
|
|
"description": "When the backup was created (ISO 8601)"
|
|
},
|
|
"source": {
|
|
"enum": [
|
|
"manual",
|
|
"scheduled",
|
|
"pre-restore"
|
|
],
|
|
"type": "string",
|
|
"description": "How the backup was created: manual, scheduled, or automatic pre-restore"
|
|
}
|
|
},
|
|
"required": [
|
|
"filename",
|
|
"sizeBytes",
|
|
"createdAt",
|
|
"source"
|
|
],
|
|
"description": "A database backup file"
|
|
},
|
|
"Job": {
|
|
"type": "object",
|
|
"properties": {
|
|
"jobName": {
|
|
"type": "string",
|
|
"description": "Cron job identifier"
|
|
},
|
|
"cronPattern": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Cron expression (e.g. 0 2 * * *)"
|
|
},
|
|
"nextRunAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Next scheduled run (ISO 8601)"
|
|
},
|
|
"lastRunAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Last run start time (ISO 8601)"
|
|
},
|
|
"lastDurationMs": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Duration of last run in milliseconds"
|
|
},
|
|
"lastStatus": {
|
|
"anyOf": [
|
|
{
|
|
"enum": [
|
|
"running",
|
|
"success",
|
|
"error"
|
|
],
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Outcome of the last run"
|
|
},
|
|
"lastError": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Error message from the last failed run"
|
|
},
|
|
"isCurrentlyRunning": {
|
|
"type": "boolean",
|
|
"description": "Whether the job is currently executing"
|
|
},
|
|
"disabled": {
|
|
"type": "boolean",
|
|
"description": "Whether the job is disabled"
|
|
}
|
|
},
|
|
"required": [
|
|
"jobName",
|
|
"cronPattern",
|
|
"nextRunAt",
|
|
"lastRunAt",
|
|
"lastDurationMs",
|
|
"lastStatus",
|
|
"lastError",
|
|
"isCurrentlyRunning",
|
|
"disabled"
|
|
],
|
|
"description": "Status of a background cron job"
|
|
},
|
|
"SystemHealth": {
|
|
"type": "object",
|
|
"properties": {
|
|
"database": {
|
|
"type": "object",
|
|
"properties": {
|
|
"dbSizeBytes": {
|
|
"type": "number",
|
|
"description": "SQLite database file size"
|
|
},
|
|
"walSizeBytes": {
|
|
"type": "number",
|
|
"description": "WAL file size"
|
|
},
|
|
"titleCount": {
|
|
"type": "number",
|
|
"description": "Total titles in database"
|
|
},
|
|
"episodeCount": {
|
|
"type": "number",
|
|
"description": "Total episodes in database"
|
|
},
|
|
"userCount": {
|
|
"type": "number",
|
|
"description": "Total registered users"
|
|
}
|
|
},
|
|
"required": [
|
|
"dbSizeBytes",
|
|
"walSizeBytes",
|
|
"titleCount",
|
|
"episodeCount",
|
|
"userCount"
|
|
],
|
|
"description": "Database size and record counts"
|
|
},
|
|
"tmdb": {
|
|
"type": "object",
|
|
"properties": {
|
|
"connected": {
|
|
"type": "boolean",
|
|
"description": "Whether TMDB API is reachable"
|
|
},
|
|
"tokenValid": {
|
|
"type": "boolean",
|
|
"description": "Whether the API token is valid"
|
|
},
|
|
"tokenConfigured": {
|
|
"type": "boolean",
|
|
"description": "Whether a token is set"
|
|
},
|
|
"responseTimeMs": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "TMDB API response time in milliseconds"
|
|
},
|
|
"error": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Error message if connectivity check failed"
|
|
}
|
|
},
|
|
"required": [
|
|
"connected",
|
|
"tokenValid",
|
|
"tokenConfigured",
|
|
"responseTimeMs",
|
|
"error"
|
|
],
|
|
"description": "TMDB API connectivity status"
|
|
},
|
|
"jobs": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/Job"
|
|
},
|
|
"description": "Status of all cron jobs"
|
|
},
|
|
"imageCache": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Whether image caching is enabled"
|
|
},
|
|
"totalSizeBytes": {
|
|
"type": "number",
|
|
"description": "Total cache size on disk"
|
|
},
|
|
"imageCount": {
|
|
"type": "number",
|
|
"description": "Total cached images"
|
|
},
|
|
"categories": {
|
|
"type": "object",
|
|
"propertyNames": {
|
|
"type": "string"
|
|
},
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"count": {
|
|
"type": "number",
|
|
"description": "Images in this category"
|
|
},
|
|
"sizeBytes": {
|
|
"type": "number",
|
|
"description": "Category size on disk"
|
|
}
|
|
},
|
|
"required": [
|
|
"count",
|
|
"sizeBytes"
|
|
]
|
|
},
|
|
"description": "Breakdown by image category (posters, backdrops, etc.)"
|
|
}
|
|
},
|
|
"required": [
|
|
"enabled",
|
|
"totalSizeBytes",
|
|
"imageCount",
|
|
"categories"
|
|
],
|
|
"description": "Image cache statistics"
|
|
},
|
|
"backups": {
|
|
"type": "object",
|
|
"properties": {
|
|
"lastBackupAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Last backup timestamp (ISO 8601)"
|
|
},
|
|
"lastBackupAgeHours": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Hours since the last backup"
|
|
},
|
|
"backupCount": {
|
|
"type": "number",
|
|
"description": "Total backup files"
|
|
},
|
|
"totalSizeBytes": {
|
|
"type": "number",
|
|
"description": "Total size of all backups"
|
|
}
|
|
},
|
|
"required": [
|
|
"lastBackupAt",
|
|
"lastBackupAgeHours",
|
|
"backupCount",
|
|
"totalSizeBytes"
|
|
],
|
|
"description": "Backup status and size information"
|
|
},
|
|
"environment": {
|
|
"type": "object",
|
|
"properties": {
|
|
"dataDir": {
|
|
"type": "string",
|
|
"description": "Configured data directory path"
|
|
},
|
|
"dataDirWritable": {
|
|
"type": "boolean",
|
|
"description": "Whether the data directory is writable"
|
|
},
|
|
"envVars": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Environment variable name"
|
|
},
|
|
"value": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Current value (sensitive values are masked)"
|
|
}
|
|
},
|
|
"required": [
|
|
"name",
|
|
"value"
|
|
]
|
|
},
|
|
"description": "Relevant environment variable statuses"
|
|
}
|
|
},
|
|
"required": [
|
|
"dataDir",
|
|
"dataDirWritable",
|
|
"envVars"
|
|
],
|
|
"description": "Server environment information"
|
|
},
|
|
"checkedAt": {
|
|
"type": "string",
|
|
"description": "When this health check was performed (ISO 8601)"
|
|
}
|
|
},
|
|
"required": [
|
|
"database",
|
|
"tmdb",
|
|
"jobs",
|
|
"imageCache",
|
|
"backups",
|
|
"environment",
|
|
"checkedAt"
|
|
],
|
|
"description": "Comprehensive system health report covering database, TMDB, jobs, cache, backups, and environment"
|
|
}
|
|
}
|
|
},
|
|
"openapi": "3.1.1",
|
|
"paths": {
|
|
"/library": {
|
|
"get": {
|
|
"operationId": "library.list",
|
|
"summary": "List library with filters",
|
|
"description": "Fetch paginated, filtered, and sorted titles from the user's library. Supports filtering by status, type, genre, rating, year, content rating, and streaming availability.",
|
|
"tags": [
|
|
"Library"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "search",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "string",
|
|
"maxLength": 200,
|
|
"description": "Search within library by title name"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "statuses",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "array",
|
|
"items": {
|
|
"enum": [
|
|
"in_watchlist",
|
|
"watching",
|
|
"caught_up",
|
|
"completed"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"description": "Filter by display statuses (multi-select)"
|
|
},
|
|
"style": "deepObject",
|
|
"explode": true,
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "type",
|
|
"in": "query",
|
|
"schema": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Filter by media type"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "genreId",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": -9007199254740991,
|
|
"maximum": 9007199254740991,
|
|
"description": "Filter by TMDB genre ID"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "ratingMin",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 5,
|
|
"description": "Minimum user star rating"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "ratingMax",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 5,
|
|
"description": "Maximum user star rating"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "yearMin",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1900,
|
|
"maximum": 2100,
|
|
"description": "Minimum release year"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "yearMax",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1900,
|
|
"maximum": 2100,
|
|
"description": "Maximum release year"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "contentRating",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "string",
|
|
"description": "Content rating filter (e.g. PG-13, TV-MA)"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "availableToStream",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "boolean",
|
|
"description": "Only show titles with streaming availability"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "sortBy",
|
|
"in": "query",
|
|
"schema": {
|
|
"enum": [
|
|
"title",
|
|
"added_at",
|
|
"release_date",
|
|
"popularity",
|
|
"user_rating",
|
|
"vote_average"
|
|
],
|
|
"type": "string",
|
|
"default": "added_at",
|
|
"description": "Sort field"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "sortDirection",
|
|
"in": "query",
|
|
"schema": {
|
|
"enum": [
|
|
"asc",
|
|
"desc"
|
|
],
|
|
"type": "string",
|
|
"default": "desc",
|
|
"description": "Sort direction"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "page",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 500,
|
|
"default": 1,
|
|
"description": "Page number (1-indexed)"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "limit",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 100,
|
|
"default": 20,
|
|
"description": "Results per page (1-100, default 20)"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Filtered library items with user statuses and ratings",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"items": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Title ID"
|
|
},
|
|
"tmdbId": {
|
|
"type": "number",
|
|
"description": "TMDB numeric ID"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Display title"
|
|
},
|
|
"posterPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Poster image path"
|
|
},
|
|
"posterThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the poster"
|
|
},
|
|
"releaseDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Release date (ISO 8601)"
|
|
},
|
|
"firstAirDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "First air date (ISO 8601)"
|
|
},
|
|
"voteAverage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Average rating (0-10)"
|
|
},
|
|
"userStatus": {
|
|
"anyOf": [
|
|
{
|
|
"enum": [
|
|
"in_watchlist",
|
|
"watching",
|
|
"caught_up",
|
|
"completed"
|
|
],
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "User's display status"
|
|
},
|
|
"userRating": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "User's star rating (1-5), or null"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"tmdbId",
|
|
"type",
|
|
"title",
|
|
"posterPath",
|
|
"posterThumbHash",
|
|
"releaseDate",
|
|
"firstAirDate",
|
|
"voteAverage",
|
|
"userStatus",
|
|
"userRating"
|
|
],
|
|
"description": "A library item with user status and rating"
|
|
}
|
|
},
|
|
"page": {
|
|
"type": "number",
|
|
"description": "Current page number"
|
|
},
|
|
"totalPages": {
|
|
"type": "number",
|
|
"description": "Total number of pages available"
|
|
},
|
|
"totalResults": {
|
|
"type": "number",
|
|
"description": "Total number of results across all pages"
|
|
}
|
|
},
|
|
"required": [
|
|
"items",
|
|
"page",
|
|
"totalPages",
|
|
"totalResults"
|
|
],
|
|
"description": "Filtered and sorted library titles"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/library/genres": {
|
|
"get": {
|
|
"operationId": "library.genres",
|
|
"summary": "List genres in user's library",
|
|
"description": "Get the distinct genres present in the user's library, ordered alphabetically. Used to populate the genre filter dropdown.",
|
|
"tags": [
|
|
"Library"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Genres present in the library",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"genres": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "number"
|
|
},
|
|
"name": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"name"
|
|
]
|
|
},
|
|
"description": "Genres present in the user's library"
|
|
}
|
|
},
|
|
"required": [
|
|
"genres"
|
|
],
|
|
"description": "Genres that exist in the user's library"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/titles/{id}": {
|
|
"get": {
|
|
"operationId": "titles.detail",
|
|
"summary": "Get title details",
|
|
"description": "Fetch full title metadata including seasons, cast, and streaming availability. Imports from TMDB on first access if not yet cached locally.",
|
|
"tags": [
|
|
"Titles"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Full title with seasons, cast, and availability offers",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"$ref": "#/components/schemas/Title"
|
|
},
|
|
"seasons": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/Season"
|
|
},
|
|
"description": "TV seasons (empty for movies)"
|
|
},
|
|
"availability": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"providerId": {
|
|
"type": "number",
|
|
"description": "JustWatch provider ID"
|
|
},
|
|
"providerName": {
|
|
"type": "string",
|
|
"description": "Display name (e.g. Netflix, Hulu)"
|
|
},
|
|
"logoPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Provider logo image path"
|
|
},
|
|
"offerType": {
|
|
"type": "string",
|
|
"description": "Offer type: flatrate, rent, buy, free, ads"
|
|
},
|
|
"watchUrl": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Direct link to watch on this provider"
|
|
}
|
|
},
|
|
"required": [
|
|
"providerId",
|
|
"providerName",
|
|
"logoPath",
|
|
"offerType",
|
|
"watchUrl"
|
|
],
|
|
"description": "A streaming availability offer from a provider"
|
|
},
|
|
"description": "Streaming availability offers"
|
|
},
|
|
"cast": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/CastMember"
|
|
},
|
|
"description": "Cast and crew credits"
|
|
}
|
|
},
|
|
"required": [
|
|
"title",
|
|
"seasons",
|
|
"availability",
|
|
"cast"
|
|
],
|
|
"description": "Full title details with seasons, cast, and streaming availability"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"404": {
|
|
"description": "404",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "NOT_FOUND"
|
|
},
|
|
"status": {
|
|
"const": 404
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Title not found"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "TITLE_NOT_FOUND"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/titles/{id}/status": {
|
|
"put": {
|
|
"operationId": "titles.updateStatus",
|
|
"summary": "Update tracking status",
|
|
"description": "Set the user's tracking status for a title. Use null to remove the title from the library entirely.",
|
|
"tags": [
|
|
"Titles"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Title ID"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"status": {
|
|
"anyOf": [
|
|
{
|
|
"enum": [
|
|
"watchlist"
|
|
],
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Set to watchlist to add to library, or null to remove. Other transitions happen via watch endpoints."
|
|
}
|
|
},
|
|
"required": [
|
|
"status"
|
|
],
|
|
"description": "Add a title to the user's watchlist or remove from library"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/titles/{id}/rating": {
|
|
"put": {
|
|
"operationId": "titles.updateRating",
|
|
"summary": "Rate a title",
|
|
"description": "Set a 0-5 star rating for a title. Use 0 to clear the rating.",
|
|
"tags": [
|
|
"Titles"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Title ID"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"stars": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"maximum": 5,
|
|
"description": "Star rating from 0 (clear) to 5"
|
|
}
|
|
},
|
|
"required": [
|
|
"stars"
|
|
],
|
|
"description": "Set or clear a star rating for a title"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/titles/{id}/watch": {
|
|
"post": {
|
|
"operationId": "titles.watchMovie",
|
|
"summary": "Mark movie as watched",
|
|
"description": "Log a watch event for a movie. Automatically sets status to completed.",
|
|
"tags": [
|
|
"Titles"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/titles/{id}/watch-all": {
|
|
"post": {
|
|
"operationId": "titles.watchAll",
|
|
"summary": "Mark all episodes watched",
|
|
"description": "Mark every episode of a TV show as watched. Requires seasons to be hydrated first.",
|
|
"tags": [
|
|
"Titles"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/titles/{id}/user-info": {
|
|
"get": {
|
|
"operationId": "titles.userInfo",
|
|
"summary": "Get user's title info",
|
|
"description": "Fetch the current user's tracking status, rating, and watched episode IDs for a title.",
|
|
"tags": [
|
|
"Titles"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "User's status, rating, and list of watched episode IDs",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"status": {
|
|
"anyOf": [
|
|
{
|
|
"enum": [
|
|
"in_watchlist",
|
|
"watching",
|
|
"caught_up",
|
|
"completed"
|
|
],
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "User's display status, or null if not in library"
|
|
},
|
|
"rating": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "User's star rating (0-5), or null if unrated"
|
|
},
|
|
"episodeWatches": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"description": "IDs of episodes the user has watched"
|
|
}
|
|
},
|
|
"required": [
|
|
"status",
|
|
"rating",
|
|
"episodeWatches"
|
|
],
|
|
"description": "The current user's tracking info for a title"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/titles/{id}/recommendations": {
|
|
"get": {
|
|
"operationId": "titles.recommendations",
|
|
"summary": "Get title recommendations",
|
|
"description": "Fetch similar titles based on locally cached recommendation data, along with the user's statuses for each.",
|
|
"tags": [
|
|
"Titles"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Recommended titles with user statuses",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"recommendations": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/Recommendation"
|
|
}
|
|
},
|
|
"userStatuses": {
|
|
"type": "object",
|
|
"propertyNames": {
|
|
"type": "string"
|
|
},
|
|
"additionalProperties": {
|
|
"enum": [
|
|
"in_watchlist",
|
|
"watching",
|
|
"caught_up",
|
|
"completed"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"description": "Map of title ID to the user's display status"
|
|
}
|
|
},
|
|
"required": [
|
|
"recommendations",
|
|
"userStatuses"
|
|
],
|
|
"description": "Recommended titles with the user's statuses"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/titles/{id}/quick-add": {
|
|
"post": {
|
|
"operationId": "titles.quickAdd",
|
|
"summary": "Quick add title to library",
|
|
"description": "Add a title to the user's watchlist and trigger a full TMDB import if needed. If the title already exists in the user's library, returns alreadyAdded: true.",
|
|
"tags": [
|
|
"Titles"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Title ID and whether it was already in the library",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Internal title ID"
|
|
},
|
|
"alreadyAdded": {
|
|
"type": "boolean",
|
|
"description": "True if the title was already in the user's library"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"alreadyAdded"
|
|
],
|
|
"description": "Result of a quick-add operation"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"404": {
|
|
"description": "404",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "NOT_FOUND"
|
|
},
|
|
"status": {
|
|
"const": 404
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Title not found"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "TITLE_NOT_FOUND"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/episodes/{id}/watch": {
|
|
"post": {
|
|
"operationId": "episodes.watch",
|
|
"summary": "Mark episode watched",
|
|
"description": "Record a watch event for a single TV episode.",
|
|
"tags": [
|
|
"Episodes"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/episodes/{id}/unwatch": {
|
|
"post": {
|
|
"operationId": "episodes.unwatch",
|
|
"summary": "Mark episode unwatched",
|
|
"description": "Remove the watch record for a single TV episode.",
|
|
"tags": [
|
|
"Episodes"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/episodes/batch-watch": {
|
|
"post": {
|
|
"operationId": "episodes.batchWatch",
|
|
"summary": "Batch mark episodes watched",
|
|
"description": "Record watch events for multiple episodes at once. Useful for marking a range of episodes as watched.",
|
|
"tags": [
|
|
"Episodes"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"episodeIds": {
|
|
"type": "array",
|
|
"minItems": 1,
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"description": "List of episode IDs to mark as watched"
|
|
}
|
|
},
|
|
"required": [
|
|
"episodeIds"
|
|
],
|
|
"description": "Batch of episode IDs to mark as watched"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/seasons/{id}/watch": {
|
|
"post": {
|
|
"operationId": "seasons.watch",
|
|
"summary": "Mark season watched",
|
|
"description": "Mark all episodes in a season as watched in a single operation.",
|
|
"tags": [
|
|
"Seasons"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/seasons/{id}/unwatch": {
|
|
"post": {
|
|
"operationId": "seasons.unwatch",
|
|
"summary": "Mark season unwatched",
|
|
"description": "Remove all watch records for episodes in a season in a single operation.",
|
|
"tags": [
|
|
"Seasons"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/people/{id}": {
|
|
"get": {
|
|
"operationId": "people.detail",
|
|
"summary": "Get person details",
|
|
"description": "Fetch a person's profile and paginated filmography. Imports from TMDB on first access if not yet cached locally.",
|
|
"tags": [
|
|
"People"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
},
|
|
{
|
|
"name": "page",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 500,
|
|
"default": 1,
|
|
"description": "Page number (1-indexed)"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "limit",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 100,
|
|
"default": 20,
|
|
"description": "Results per page (1-100, default 20)"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Person profile, paginated filmography credits, and user statuses for their titles",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"person": {
|
|
"$ref": "#/components/schemas/Person"
|
|
},
|
|
"filmography": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/PersonCredit"
|
|
},
|
|
"description": "Credits for this person (paginated)"
|
|
},
|
|
"userStatuses": {
|
|
"type": "object",
|
|
"propertyNames": {
|
|
"type": "string"
|
|
},
|
|
"additionalProperties": {
|
|
"enum": [
|
|
"in_watchlist",
|
|
"watching",
|
|
"caught_up",
|
|
"completed"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"description": "Map of title ID to the user's display status"
|
|
},
|
|
"page": {
|
|
"type": "number",
|
|
"description": "Current page number"
|
|
},
|
|
"totalPages": {
|
|
"type": "number",
|
|
"description": "Total number of pages available"
|
|
},
|
|
"totalResults": {
|
|
"type": "number",
|
|
"description": "Total number of results across all pages"
|
|
}
|
|
},
|
|
"required": [
|
|
"person",
|
|
"filmography",
|
|
"userStatuses",
|
|
"page",
|
|
"totalPages",
|
|
"totalResults"
|
|
],
|
|
"description": "Person profile with paginated filmography and user's statuses for their titles"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"404": {
|
|
"description": "404",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "NOT_FOUND"
|
|
},
|
|
"status": {
|
|
"const": 404
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Person not found"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "PERSON_NOT_FOUND"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/dashboard/stats": {
|
|
"get": {
|
|
"operationId": "dashboard.stats",
|
|
"summary": "Get dashboard statistics",
|
|
"description": "Fetch summary counts for the current user: movies watched this month, episodes this week, library size, and completed titles.",
|
|
"tags": [
|
|
"Dashboard"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Aggregate watch statistics",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"moviesThisMonth": {
|
|
"type": "number",
|
|
"description": "Movies watched in the current calendar month"
|
|
},
|
|
"episodesThisWeek": {
|
|
"type": "number",
|
|
"description": "Episodes watched in the current calendar week"
|
|
},
|
|
"librarySize": {
|
|
"type": "number",
|
|
"description": "Total titles in the user's library"
|
|
},
|
|
"completed": {
|
|
"type": "number",
|
|
"description": "Total titles with completed status"
|
|
}
|
|
},
|
|
"required": [
|
|
"moviesThisMonth",
|
|
"episodesThisWeek",
|
|
"librarySize",
|
|
"completed"
|
|
],
|
|
"description": "Aggregate watch statistics for the dashboard"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/dashboard/continue-watching": {
|
|
"get": {
|
|
"operationId": "dashboard.continueWatching",
|
|
"summary": "Get continue watching list",
|
|
"description": "Fetch TV shows the user is currently watching, with the next unwatched episode and progress for each.",
|
|
"tags": [
|
|
"Dashboard"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "In-progress shows with next episode and watch progress",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"items": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Title ID"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Display title"
|
|
},
|
|
"backdropPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Backdrop image path"
|
|
},
|
|
"backdropThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the backdrop"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"title",
|
|
"backdropPath",
|
|
"backdropThumbHash"
|
|
]
|
|
},
|
|
"nextEpisode": {
|
|
"anyOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"seasonNumber": {
|
|
"type": "number",
|
|
"description": "Season number"
|
|
},
|
|
"episodeNumber": {
|
|
"type": "number",
|
|
"description": "Episode number"
|
|
},
|
|
"name": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Episode title"
|
|
},
|
|
"stillPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Episode still image path"
|
|
},
|
|
"stillThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for the still"
|
|
}
|
|
},
|
|
"required": [
|
|
"seasonNumber",
|
|
"episodeNumber",
|
|
"name",
|
|
"stillPath",
|
|
"stillThumbHash"
|
|
]
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Next unwatched episode, or null if all caught up"
|
|
},
|
|
"totalEpisodes": {
|
|
"type": "number",
|
|
"description": "Total episodes across all seasons"
|
|
},
|
|
"watchedEpisodes": {
|
|
"type": "number",
|
|
"description": "Episodes the user has watched"
|
|
}
|
|
},
|
|
"required": [
|
|
"title",
|
|
"nextEpisode",
|
|
"totalEpisodes",
|
|
"watchedEpisodes"
|
|
],
|
|
"description": "An in-progress show with watch progress"
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"items"
|
|
],
|
|
"description": "TV shows the user is currently watching with next episode info"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/dashboard/upcoming": {
|
|
"get": {
|
|
"operationId": "dashboard.upcoming",
|
|
"summary": "Get upcoming episodes and movies",
|
|
"description": "Fetch upcoming episodes and movie releases for titles in the user's library, sorted by date. Supports cursor-based pagination.",
|
|
"tags": [
|
|
"Dashboard"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "days",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 90,
|
|
"default": 90,
|
|
"description": "How many days into the future to look"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "limit",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 50,
|
|
"default": 20,
|
|
"description": "Maximum items per page"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "cursor",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "string",
|
|
"description": "Pagination cursor"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "mediaType",
|
|
"in": "query",
|
|
"schema": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Filter to only movies or only TV episodes"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "statusFilter",
|
|
"in": "query",
|
|
"schema": {
|
|
"type": "array",
|
|
"items": {
|
|
"enum": [
|
|
"watching",
|
|
"watchlist"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"description": "Filter by user tracking status"
|
|
},
|
|
"style": "deepObject",
|
|
"explode": true,
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Upcoming items sorted by date with streaming info",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"items": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"episodeId": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Episode ID (TV only, null for movies and collapsed batches)"
|
|
},
|
|
"titleId": {
|
|
"type": "string",
|
|
"description": "Internal title ID"
|
|
},
|
|
"titleName": {
|
|
"type": "string",
|
|
"description": "Display title"
|
|
},
|
|
"titleType": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type"
|
|
},
|
|
"posterPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Poster image path"
|
|
},
|
|
"posterThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder"
|
|
},
|
|
"backdropPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Backdrop image path"
|
|
},
|
|
"backdropThumbHash": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ThumbHash blur placeholder for backdrop"
|
|
},
|
|
"seasonNumber": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Season number (TV only)"
|
|
},
|
|
"episodeNumber": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Episode number (TV only)"
|
|
},
|
|
"episodeName": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Episode title (TV only)"
|
|
},
|
|
"episodeCount": {
|
|
"type": "number",
|
|
"description": "Number of episodes (1 for single, >1 for collapsed batch drops)"
|
|
},
|
|
"date": {
|
|
"type": "string",
|
|
"description": "Air date or release date (YYYY-MM-DD)"
|
|
},
|
|
"userStatus": {
|
|
"enum": [
|
|
"in_watchlist",
|
|
"watching",
|
|
"caught_up",
|
|
"completed"
|
|
],
|
|
"type": "string",
|
|
"description": "User's display status"
|
|
},
|
|
"isNewSeason": {
|
|
"type": "boolean",
|
|
"description": "Whether this is a new season for a completed show"
|
|
},
|
|
"streamingProvider": {
|
|
"anyOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"providerId": {
|
|
"type": "number"
|
|
},
|
|
"providerName": {
|
|
"type": "string"
|
|
},
|
|
"logoPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"providerId",
|
|
"providerName",
|
|
"logoPath"
|
|
]
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Primary streaming provider, or null"
|
|
}
|
|
},
|
|
"required": [
|
|
"episodeId",
|
|
"titleId",
|
|
"titleName",
|
|
"titleType",
|
|
"posterPath",
|
|
"posterThumbHash",
|
|
"backdropPath",
|
|
"backdropThumbHash",
|
|
"seasonNumber",
|
|
"episodeNumber",
|
|
"episodeName",
|
|
"episodeCount",
|
|
"date",
|
|
"userStatus",
|
|
"isNewSeason",
|
|
"streamingProvider"
|
|
],
|
|
"description": "An upcoming episode or movie release"
|
|
}
|
|
},
|
|
"nextCursor": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Cursor for the next page, or null if no more items"
|
|
}
|
|
},
|
|
"required": [
|
|
"items",
|
|
"nextCursor"
|
|
],
|
|
"description": "Upcoming episodes and movie releases for tracked titles"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/dashboard/recommendations": {
|
|
"get": {
|
|
"operationId": "dashboard.recommendations",
|
|
"summary": "Get personalized recommendations",
|
|
"description": "Fetch personalized title recommendations based on the user's library and watch history.",
|
|
"tags": [
|
|
"Dashboard"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Recommended titles",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"items": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/Recommendation"
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"items"
|
|
],
|
|
"description": "Personalized title recommendations for the dashboard"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/dashboard/watch-history": {
|
|
"get": {
|
|
"operationId": "dashboard.watchHistory",
|
|
"summary": "Get watch history",
|
|
"description": "Fetch the user's watch counts grouped by time period. Useful for rendering activity charts.",
|
|
"tags": [
|
|
"Dashboard"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "type",
|
|
"in": "query",
|
|
"required": true,
|
|
"schema": {
|
|
"enum": [
|
|
"movie",
|
|
"episode"
|
|
],
|
|
"type": "string",
|
|
"description": "What to count: movie watches or episode watches"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "period",
|
|
"in": "query",
|
|
"required": true,
|
|
"schema": {
|
|
"enum": [
|
|
"today",
|
|
"this_week",
|
|
"this_month",
|
|
"this_year"
|
|
],
|
|
"type": "string",
|
|
"description": "Time range for the histogram"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Watch counts bucketed by time period",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"count": {
|
|
"type": "number",
|
|
"description": "Total watches in the selected period"
|
|
},
|
|
"history": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"bucket": {
|
|
"type": "string",
|
|
"description": "Time period label (e.g. date or week)"
|
|
},
|
|
"count": {
|
|
"type": "number",
|
|
"description": "Number of watches in this bucket"
|
|
}
|
|
},
|
|
"required": [
|
|
"bucket",
|
|
"count"
|
|
],
|
|
"description": "A single time-bucketed watch count"
|
|
},
|
|
"description": "Watch counts bucketed by time period"
|
|
}
|
|
},
|
|
"required": [
|
|
"count",
|
|
"history"
|
|
],
|
|
"description": "Watch history with time-bucketed counts"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/explore/trending": {
|
|
"get": {
|
|
"operationId": "explore.trending",
|
|
"summary": "Get trending titles",
|
|
"description": "Fetch today's trending movies and/or TV shows from TMDB, including a featured hero title and the user's statuses.",
|
|
"tags": [
|
|
"Explore"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "type",
|
|
"in": "query",
|
|
"required": true,
|
|
"schema": {
|
|
"enum": [
|
|
"all",
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Trending category: all, movie, or tv"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "page",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 500,
|
|
"default": 1,
|
|
"description": "Page number (1-indexed)"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Trending items, hero spotlight, and user statuses",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"items": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/BrowseItem"
|
|
},
|
|
"description": "Trending titles"
|
|
},
|
|
"hero": {
|
|
"anyOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Internal title ID"
|
|
},
|
|
"tmdbId": {
|
|
"type": "number",
|
|
"description": "TMDB numeric ID"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Display title"
|
|
},
|
|
"overview": {
|
|
"type": "string",
|
|
"description": "Plot synopsis"
|
|
},
|
|
"backdropPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Backdrop image path"
|
|
},
|
|
"voteAverage": {
|
|
"type": "number",
|
|
"description": "Average rating (0-10)"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"tmdbId",
|
|
"type",
|
|
"title",
|
|
"overview",
|
|
"backdropPath",
|
|
"voteAverage"
|
|
]
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Featured hero title for the spotlight banner"
|
|
},
|
|
"userStatuses": {
|
|
"type": "object",
|
|
"propertyNames": {
|
|
"type": "string"
|
|
},
|
|
"additionalProperties": {
|
|
"enum": [
|
|
"in_watchlist",
|
|
"watching",
|
|
"caught_up",
|
|
"completed"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"description": "Map of title ID to the user's display status"
|
|
},
|
|
"episodeProgress": {
|
|
"type": "object",
|
|
"propertyNames": {
|
|
"type": "string"
|
|
},
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"watched": {
|
|
"type": "number"
|
|
},
|
|
"total": {
|
|
"type": "number"
|
|
}
|
|
},
|
|
"required": [
|
|
"watched",
|
|
"total"
|
|
]
|
|
},
|
|
"description": "Map of title ID to episode watch progress"
|
|
},
|
|
"page": {
|
|
"type": "number",
|
|
"description": "Current page number"
|
|
},
|
|
"totalPages": {
|
|
"type": "number",
|
|
"description": "Total number of pages available"
|
|
},
|
|
"totalResults": {
|
|
"type": "number",
|
|
"description": "Total number of results across all pages"
|
|
}
|
|
},
|
|
"required": [
|
|
"items",
|
|
"hero",
|
|
"userStatuses",
|
|
"episodeProgress",
|
|
"page",
|
|
"totalPages",
|
|
"totalResults"
|
|
],
|
|
"description": "Trending titles with hero spotlight and user statuses"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"412": {
|
|
"description": "412",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "PRECONDITION_FAILED"
|
|
},
|
|
"status": {
|
|
"const": 412
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "TMDB API key is not configured"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "TMDB_NOT_CONFIGURED"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/explore/popular": {
|
|
"get": {
|
|
"operationId": "explore.popular",
|
|
"summary": "Get popular titles",
|
|
"description": "Fetch currently popular movies or TV shows from TMDB with the user's tracking statuses.",
|
|
"tags": [
|
|
"Explore"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "type",
|
|
"in": "query",
|
|
"required": true,
|
|
"schema": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type filter"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "page",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 500,
|
|
"default": 1,
|
|
"description": "Page number (1-indexed)"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Popular items with user statuses",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"items": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/BrowseItem"
|
|
}
|
|
},
|
|
"userStatuses": {
|
|
"type": "object",
|
|
"propertyNames": {
|
|
"type": "string"
|
|
},
|
|
"additionalProperties": {
|
|
"enum": [
|
|
"in_watchlist",
|
|
"watching",
|
|
"caught_up",
|
|
"completed"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"description": "Map of title ID to the user's display status"
|
|
},
|
|
"episodeProgress": {
|
|
"type": "object",
|
|
"propertyNames": {
|
|
"type": "string"
|
|
},
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"watched": {
|
|
"type": "number"
|
|
},
|
|
"total": {
|
|
"type": "number"
|
|
}
|
|
},
|
|
"required": [
|
|
"watched",
|
|
"total"
|
|
]
|
|
},
|
|
"description": "Map of title ID to episode watch progress"
|
|
},
|
|
"page": {
|
|
"type": "number",
|
|
"description": "Current page number"
|
|
},
|
|
"totalPages": {
|
|
"type": "number",
|
|
"description": "Total number of pages available"
|
|
},
|
|
"totalResults": {
|
|
"type": "number",
|
|
"description": "Total number of results across all pages"
|
|
}
|
|
},
|
|
"required": [
|
|
"items",
|
|
"userStatuses",
|
|
"episodeProgress",
|
|
"page",
|
|
"totalPages",
|
|
"totalResults"
|
|
],
|
|
"description": "Browse results with user tracking statuses and episode progress"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"412": {
|
|
"description": "412",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "PRECONDITION_FAILED"
|
|
},
|
|
"status": {
|
|
"const": 412
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "TMDB API key is not configured"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "TMDB_NOT_CONFIGURED"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/explore/genres": {
|
|
"get": {
|
|
"operationId": "explore.genres",
|
|
"summary": "List genres",
|
|
"description": "Fetch the list of TMDB genres for movies or TV shows. Used to populate genre filters for discovery.",
|
|
"tags": [
|
|
"Explore"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "type",
|
|
"in": "query",
|
|
"required": true,
|
|
"schema": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type filter"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Genre ID/name pairs",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"genres": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "number",
|
|
"description": "TMDB genre ID"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Genre display name"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"name"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"genres"
|
|
],
|
|
"description": "Available genres for filtering"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"412": {
|
|
"description": "412",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "PRECONDITION_FAILED"
|
|
},
|
|
"status": {
|
|
"const": 412
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "TMDB API key is not configured"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "TMDB_NOT_CONFIGURED"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/explore/watch-providers": {
|
|
"get": {
|
|
"operationId": "explore.watchProviders",
|
|
"summary": "List available watch providers",
|
|
"description": "Fetch the list of streaming providers available in the configured region. Used to populate provider filter dropdowns.",
|
|
"tags": [
|
|
"Explore"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "type",
|
|
"in": "query",
|
|
"required": true,
|
|
"schema": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type filter"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Provider list with logos",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"providers": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "number",
|
|
"description": "TMDB provider ID"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Provider display name"
|
|
},
|
|
"logoPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Provider logo image path"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"name",
|
|
"logoPath"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"providers"
|
|
],
|
|
"description": "Available streaming providers for the user's region"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"412": {
|
|
"description": "412",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "PRECONDITION_FAILED"
|
|
},
|
|
"status": {
|
|
"const": 412
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "TMDB API key is not configured"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "TMDB_NOT_CONFIGURED"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/search": {
|
|
"get": {
|
|
"operationId": "search",
|
|
"summary": "Search movies, TV shows, and people",
|
|
"description": "Full-text search across movies, TV shows, and people via TMDB. Optionally filter by media type.",
|
|
"tags": [
|
|
"Search"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "query",
|
|
"in": "query",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 200,
|
|
"description": "Search query text"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "type",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"enum": [
|
|
"movie",
|
|
"tv",
|
|
"person"
|
|
],
|
|
"type": "string",
|
|
"description": "Optional type filter to narrow results"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "page",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 500,
|
|
"default": 1,
|
|
"description": "Page number (1-indexed)"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Search results with metadata",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"results": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Internal title ID (present for movie/tv results, absent for people)"
|
|
},
|
|
"tmdbId": {
|
|
"type": "number",
|
|
"description": "TMDB numeric ID"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv",
|
|
"person"
|
|
],
|
|
"type": "string",
|
|
"description": "Result type"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Title or person name"
|
|
},
|
|
"overview": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Plot summary or null for people"
|
|
},
|
|
"posterPath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Poster image path (movies/TV)"
|
|
},
|
|
"profilePath": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Profile photo path (people)"
|
|
},
|
|
"releaseDate": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Release date (ISO 8601)"
|
|
},
|
|
"popularity": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "TMDB popularity score"
|
|
},
|
|
"voteAverage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Average rating (0-10)"
|
|
},
|
|
"knownForDepartment": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Primary department (people only)"
|
|
},
|
|
"knownFor": {
|
|
"anyOf": [
|
|
{
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Notable works (people only, up to 3 titles)"
|
|
}
|
|
},
|
|
"required": [
|
|
"tmdbId",
|
|
"type",
|
|
"title",
|
|
"overview",
|
|
"posterPath",
|
|
"profilePath",
|
|
"releaseDate",
|
|
"popularity",
|
|
"voteAverage",
|
|
"knownForDepartment",
|
|
"knownFor"
|
|
],
|
|
"description": "A search result (movie, TV show, or person)"
|
|
}
|
|
},
|
|
"page": {
|
|
"type": "number",
|
|
"description": "Current page number"
|
|
},
|
|
"totalPages": {
|
|
"type": "number",
|
|
"description": "Total number of pages available"
|
|
},
|
|
"totalResults": {
|
|
"type": "number",
|
|
"description": "Total number of results across all pages"
|
|
}
|
|
},
|
|
"required": [
|
|
"results",
|
|
"page",
|
|
"totalPages",
|
|
"totalResults"
|
|
],
|
|
"description": "Search results from TMDB"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"412": {
|
|
"description": "412",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "PRECONDITION_FAILED"
|
|
},
|
|
"status": {
|
|
"const": 412
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "TMDB API key is not configured"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/discover": {
|
|
"get": {
|
|
"operationId": "discover",
|
|
"summary": "Discover titles by genre",
|
|
"description": "Browse movies or TV shows filtered by genre, sorted by popularity. Returns user statuses and episode progress.",
|
|
"tags": [
|
|
"Discover"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "type",
|
|
"in": "query",
|
|
"required": true,
|
|
"schema": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string",
|
|
"description": "Media type to discover"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "genreId",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": -9007199254740991,
|
|
"maximum": 9007199254740991,
|
|
"description": "TMDB genre ID to filter by"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "yearMin",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1900,
|
|
"maximum": 2100,
|
|
"description": "Minimum release year"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "yearMax",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1900,
|
|
"maximum": 2100,
|
|
"description": "Maximum release year"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "ratingMin",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "number",
|
|
"minimum": 0,
|
|
"maximum": 10,
|
|
"description": "Minimum TMDB vote average"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "sortBy",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"enum": [
|
|
"popularity.desc",
|
|
"vote_average.desc",
|
|
"primary_release_date.desc",
|
|
"primary_release_date.asc"
|
|
],
|
|
"type": "string",
|
|
"description": "Sort order for results"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "language",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "string",
|
|
"description": "ISO 639-1 original language code"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "providerId",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": -9007199254740991,
|
|
"maximum": 9007199254740991,
|
|
"description": "TMDB watch provider ID"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
},
|
|
{
|
|
"name": "page",
|
|
"in": "query",
|
|
"required": false,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 500,
|
|
"default": 1,
|
|
"description": "Page number (1-indexed)"
|
|
},
|
|
"allowEmptyValue": true,
|
|
"allowReserved": true
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Filtered titles with user statuses",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"items": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/BrowseItem"
|
|
}
|
|
},
|
|
"userStatuses": {
|
|
"type": "object",
|
|
"propertyNames": {
|
|
"type": "string"
|
|
},
|
|
"additionalProperties": {
|
|
"enum": [
|
|
"in_watchlist",
|
|
"watching",
|
|
"caught_up",
|
|
"completed"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"description": "Map of title ID to the user's display status"
|
|
},
|
|
"episodeProgress": {
|
|
"type": "object",
|
|
"propertyNames": {
|
|
"type": "string"
|
|
},
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"watched": {
|
|
"type": "number"
|
|
},
|
|
"total": {
|
|
"type": "number"
|
|
}
|
|
},
|
|
"required": [
|
|
"watched",
|
|
"total"
|
|
]
|
|
},
|
|
"description": "Map of title ID to episode watch progress"
|
|
},
|
|
"page": {
|
|
"type": "number",
|
|
"description": "Current page number"
|
|
},
|
|
"totalPages": {
|
|
"type": "number",
|
|
"description": "Total number of pages available"
|
|
},
|
|
"totalResults": {
|
|
"type": "number",
|
|
"description": "Total number of results across all pages"
|
|
}
|
|
},
|
|
"required": [
|
|
"items",
|
|
"userStatuses",
|
|
"episodeProgress",
|
|
"page",
|
|
"totalPages",
|
|
"totalResults"
|
|
],
|
|
"description": "Browse results with user tracking statuses and episode progress"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"412": {
|
|
"description": "412",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "PRECONDITION_FAILED"
|
|
},
|
|
"status": {
|
|
"const": 412
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "TMDB API key is not configured"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "TMDB_NOT_CONFIGURED"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/system/public-info": {
|
|
"get": {
|
|
"operationId": "system.publicInfo",
|
|
"summary": "Get public instance info",
|
|
"description": "Fetch public information about this Sofa instance. Does not require authentication. Used by the login screen to display instance details.",
|
|
"tags": [
|
|
"System"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Instance configuration and status",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"instanceId": {
|
|
"type": "string",
|
|
"description": "Unique instance identifier"
|
|
},
|
|
"tmdbConfigured": {
|
|
"type": "boolean",
|
|
"description": "Whether TMDB API is configured"
|
|
},
|
|
"userCount": {
|
|
"type": "number",
|
|
"description": "Number of registered users"
|
|
},
|
|
"registrationOpen": {
|
|
"type": "boolean",
|
|
"description": "Whether new user registration is open"
|
|
},
|
|
"posterUrls": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"description": "Poster image URLs for the login screen collage"
|
|
}
|
|
},
|
|
"required": [
|
|
"instanceId",
|
|
"tmdbConfigured",
|
|
"userCount",
|
|
"registrationOpen",
|
|
"posterUrls"
|
|
],
|
|
"description": "Public instance information shown on the login/setup screen"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"/system/auth-config": {
|
|
"get": {
|
|
"operationId": "system.authConfig",
|
|
"summary": "Get authentication config",
|
|
"description": "Fetch the authentication configuration including OIDC availability, password login status, and registration state. Does not require authentication.",
|
|
"tags": [
|
|
"System"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Authentication provider configuration",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"oidcEnabled": {
|
|
"type": "boolean",
|
|
"description": "Whether OIDC/SSO login is available"
|
|
},
|
|
"oidcProviderName": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Display name of the OIDC provider (e.g. Authelia, Keycloak)"
|
|
},
|
|
"passwordLoginDisabled": {
|
|
"type": "boolean",
|
|
"description": "Whether password-based login is disabled"
|
|
},
|
|
"registrationOpen": {
|
|
"type": "boolean",
|
|
"description": "Whether new user registration is open"
|
|
},
|
|
"userCount": {
|
|
"type": "number",
|
|
"description": "Number of registered users"
|
|
}
|
|
},
|
|
"required": [
|
|
"oidcEnabled",
|
|
"oidcProviderName",
|
|
"passwordLoginDisabled",
|
|
"registrationOpen",
|
|
"userCount"
|
|
],
|
|
"description": "Authentication provider configuration"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"/system/status": {
|
|
"get": {
|
|
"operationId": "system.status",
|
|
"summary": "Get internal system config",
|
|
"description": "Returns internal configuration such as the public API URL. Requires authentication.",
|
|
"tags": [
|
|
"System"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Internal system configuration",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"publicApiUrl": {
|
|
"type": "string",
|
|
"description": "Base URL of the centralized public API"
|
|
}
|
|
},
|
|
"required": [
|
|
"publicApiUrl"
|
|
],
|
|
"description": "Internal system configuration for authenticated clients"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/integrations": {
|
|
"get": {
|
|
"operationId": "integrations.list",
|
|
"summary": "List integrations",
|
|
"description": "Fetch all configured media server integrations for the current user, including recent webhook/sync events for each.",
|
|
"tags": [
|
|
"Integrations"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Integrations with their recent events",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"integrations": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Integration ID"
|
|
},
|
|
"provider": {
|
|
"type": "string",
|
|
"description": "Provider name (plex, jellyfin, etc.)"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"webhook",
|
|
"list"
|
|
],
|
|
"type": "string",
|
|
"description": "Integration type: webhook (Plex/Jellyfin/Emby) or list (Sonarr/Radarr)"
|
|
},
|
|
"token": {
|
|
"type": "string",
|
|
"description": "Webhook authentication token"
|
|
},
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Whether the integration is active"
|
|
},
|
|
"lastEventAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Last received event timestamp (ISO 8601)"
|
|
},
|
|
"createdAt": {
|
|
"type": "string",
|
|
"description": "When the integration was created (ISO 8601)"
|
|
},
|
|
"recentEvents": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/IntegrationEvent"
|
|
},
|
|
"description": "Last 10 events for this integration"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"provider",
|
|
"type",
|
|
"token",
|
|
"enabled",
|
|
"lastEventAt",
|
|
"createdAt",
|
|
"recentEvents"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"integrations"
|
|
],
|
|
"description": "All integrations with their recent events"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
},
|
|
"post": {
|
|
"operationId": "integrations.create",
|
|
"summary": "Create or update integration",
|
|
"description": "Create a new media server integration or update an existing one. Generates a unique webhook token for the provider.",
|
|
"tags": [
|
|
"Integrations"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"provider": {
|
|
"enum": [
|
|
"plex",
|
|
"jellyfin",
|
|
"emby",
|
|
"sonarr",
|
|
"radarr"
|
|
],
|
|
"type": "string",
|
|
"description": "Media server provider to integrate"
|
|
},
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Whether the integration starts enabled (default: true)"
|
|
}
|
|
},
|
|
"required": [
|
|
"provider"
|
|
],
|
|
"description": "Create a new media server integration"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Created or updated integration with token",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/Integration"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/integrations/{provider}": {
|
|
"delete": {
|
|
"operationId": "integrations.delete",
|
|
"summary": "Delete integration",
|
|
"description": "Remove a media server integration and all its event history.",
|
|
"tags": [
|
|
"Integrations"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "provider",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"enum": [
|
|
"plex",
|
|
"jellyfin",
|
|
"emby",
|
|
"sonarr",
|
|
"radarr"
|
|
],
|
|
"type": "string",
|
|
"description": "Media server provider type"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/integrations/{provider}/regenerate-token": {
|
|
"post": {
|
|
"operationId": "integrations.regenerateToken",
|
|
"summary": "Regenerate webhook token",
|
|
"description": "Generate a new webhook token for an integration. The old token is immediately invalidated.",
|
|
"tags": [
|
|
"Integrations"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "provider",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"enum": [
|
|
"plex",
|
|
"jellyfin",
|
|
"emby",
|
|
"sonarr",
|
|
"radarr"
|
|
],
|
|
"type": "string",
|
|
"description": "Media server provider type"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Integration with new token",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/Integration"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"404": {
|
|
"description": "404",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "NOT_FOUND"
|
|
},
|
|
"status": {
|
|
"const": 404
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Integration not found"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "INTEGRATION_NOT_FOUND"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/backups": {
|
|
"get": {
|
|
"operationId": "admin.backups.list",
|
|
"summary": "List backups",
|
|
"description": "Fetch all database backups with their sizes and creation times.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Available backups",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"backups": {
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/components/schemas/Backup"
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"backups"
|
|
],
|
|
"description": "All available database backups"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
},
|
|
"post": {
|
|
"operationId": "admin.backups.create",
|
|
"summary": "Create backup",
|
|
"description": "Create a new manual database backup. The backup is tagged as a manual backup.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Created backup metadata",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/Backup"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/backups/{filename}": {
|
|
"delete": {
|
|
"operationId": "admin.backups.delete",
|
|
"summary": "Delete backup",
|
|
"description": "Permanently delete a database backup file by filename.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "filename",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Backup filename"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
},
|
|
"400": {
|
|
"description": "400",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "BAD_REQUEST"
|
|
},
|
|
"status": {
|
|
"const": 400
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Failed to delete backup"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "BACKUP_DELETE_FAILED"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"404": {
|
|
"description": "404",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "NOT_FOUND"
|
|
},
|
|
"status": {
|
|
"const": 404
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Backup not found"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "BACKUP_NOT_FOUND"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/backups/restore": {
|
|
"post": {
|
|
"operationId": "admin.backups.restore",
|
|
"summary": "Restore from backup",
|
|
"description": "Upload a database backup file and restore it. A pre-restore backup is automatically created before overwriting the current database.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"multipart/form-data": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"file": {
|
|
"type": "string",
|
|
"format": "binary"
|
|
}
|
|
},
|
|
"required": [
|
|
"file"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
},
|
|
"400": {
|
|
"description": "400",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "BAD_REQUEST"
|
|
},
|
|
"status": {
|
|
"const": 400
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Backup restoration failed"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "BACKUP_RESTORE_FAILED"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/backups/schedule": {
|
|
"get": {
|
|
"operationId": "admin.backups.schedule",
|
|
"summary": "Get backup schedule",
|
|
"description": "Fetch the current automated backup schedule configuration.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Backup schedule settings",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Whether scheduled backups are enabled"
|
|
},
|
|
"maxRetention": {
|
|
"type": "number",
|
|
"description": "Maximum backups to keep (0 = unlimited)"
|
|
},
|
|
"frequency": {
|
|
"enum": [
|
|
"6h",
|
|
"12h",
|
|
"1d",
|
|
"7d"
|
|
],
|
|
"type": "string",
|
|
"description": "Backup interval"
|
|
},
|
|
"time": {
|
|
"type": "string",
|
|
"description": "Scheduled time (HH:MM, 24-hour format)"
|
|
},
|
|
"dayOfWeek": {
|
|
"type": "number",
|
|
"description": "Day of week for weekly backups (0 = Sunday)"
|
|
}
|
|
},
|
|
"required": [
|
|
"enabled",
|
|
"maxRetention",
|
|
"frequency",
|
|
"time",
|
|
"dayOfWeek"
|
|
],
|
|
"description": "Automated backup schedule configuration"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
},
|
|
"put": {
|
|
"operationId": "admin.backups.updateSchedule",
|
|
"summary": "Update backup schedule",
|
|
"description": "Update the automated backup schedule. Only provided fields are changed; omitted fields keep their current values.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable or disable scheduled backups"
|
|
},
|
|
"frequency": {
|
|
"enum": [
|
|
"6h",
|
|
"12h",
|
|
"1d",
|
|
"7d"
|
|
],
|
|
"type": "string",
|
|
"description": "Backup interval"
|
|
},
|
|
"time": {
|
|
"type": "string",
|
|
"pattern": "^\\d{2}:\\d{2}$",
|
|
"description": "Time of day to run backups (HH:MM, 24-hour format)"
|
|
},
|
|
"dayOfWeek": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"maximum": 6,
|
|
"description": "Day of week for weekly backups (0 = Sunday, 6 = Saturday)"
|
|
},
|
|
"maxRetention": {
|
|
"type": "integer",
|
|
"minimum": -9007199254740991,
|
|
"maximum": 9007199254740991,
|
|
"description": "Maximum number of backups to keep (0 = unlimited, 1-30 otherwise)"
|
|
}
|
|
},
|
|
"description": "Partial update to the automated backup schedule"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/registration": {
|
|
"get": {
|
|
"operationId": "admin.registration",
|
|
"summary": "Get registration status",
|
|
"description": "Check whether new user registration is currently open or closed.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Registration open/closed state",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"open": {
|
|
"type": "boolean",
|
|
"description": "Whether new user registration is open"
|
|
}
|
|
},
|
|
"required": [
|
|
"open"
|
|
],
|
|
"description": "User registration status"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
},
|
|
"put": {
|
|
"operationId": "admin.toggleRegistration",
|
|
"summary": "Toggle registration",
|
|
"description": "Open or close new user registration.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"open": {
|
|
"type": "boolean",
|
|
"description": "Whether new user registration is allowed"
|
|
}
|
|
},
|
|
"required": [
|
|
"open"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/update-check": {
|
|
"get": {
|
|
"operationId": "admin.updateCheck",
|
|
"summary": "Get update check status",
|
|
"description": "Fetch whether automatic update checks are enabled, and the latest cached check result if available.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Update check configuration and latest result",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Whether automatic update checks are enabled"
|
|
},
|
|
"updateCheck": {
|
|
"anyOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"updateAvailable": {
|
|
"type": "boolean",
|
|
"description": "Whether a newer version is available"
|
|
},
|
|
"currentVersion": {
|
|
"type": "string",
|
|
"description": "Currently running version"
|
|
},
|
|
"latestVersion": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Latest available version, or null if check failed"
|
|
},
|
|
"releaseUrl": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "URL to the latest release page"
|
|
},
|
|
"lastCheckedAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "When the last check was performed (ISO 8601)"
|
|
}
|
|
},
|
|
"required": [
|
|
"updateAvailable",
|
|
"currentVersion",
|
|
"latestVersion",
|
|
"releaseUrl",
|
|
"lastCheckedAt"
|
|
],
|
|
"description": "Result of an update availability check"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "Latest check result, or null if checks are disabled or never ran"
|
|
}
|
|
},
|
|
"required": [
|
|
"enabled",
|
|
"updateCheck"
|
|
],
|
|
"description": "Update check configuration and latest result"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
},
|
|
"put": {
|
|
"operationId": "admin.toggleUpdateCheck",
|
|
"summary": "Toggle update checks",
|
|
"description": "Enable or disable automatic update checks against the public API.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Whether automatic update checks are enabled"
|
|
}
|
|
},
|
|
"required": [
|
|
"enabled"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/telemetry": {
|
|
"get": {
|
|
"operationId": "admin.telemetry",
|
|
"summary": "Get telemetry status",
|
|
"description": "Fetch whether anonymous telemetry is enabled and when the last report was sent.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Telemetry configuration and last report time",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Whether telemetry is currently enabled"
|
|
},
|
|
"lastReportedAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
],
|
|
"description": "ISO 8601 timestamp of the last telemetry report, or null if never sent"
|
|
}
|
|
},
|
|
"required": [
|
|
"enabled",
|
|
"lastReportedAt"
|
|
],
|
|
"description": "Telemetry configuration and last report time"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
},
|
|
"put": {
|
|
"operationId": "admin.toggleTelemetry",
|
|
"summary": "Toggle telemetry",
|
|
"description": "Enable or disable anonymous telemetry reporting.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Whether anonymous telemetry reporting is enabled"
|
|
}
|
|
},
|
|
"required": [
|
|
"enabled"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/jobs/trigger": {
|
|
"post": {
|
|
"operationId": "admin.triggerJob",
|
|
"summary": "Trigger cron job",
|
|
"description": "Manually trigger a background cron job by name. The job runs asynchronously.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"enum": [
|
|
"scheduledBackup",
|
|
"nightlyRefreshLibrary",
|
|
"refreshAvailability",
|
|
"refreshRecommendations",
|
|
"refreshTvChildren",
|
|
"cacheImages",
|
|
"refreshCredits",
|
|
"updateCheck",
|
|
"telemetryReport"
|
|
],
|
|
"type": "string",
|
|
"description": "Cron job to trigger"
|
|
}
|
|
},
|
|
"required": [
|
|
"name"
|
|
],
|
|
"description": "Specify which background job to trigger manually"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"ok": {
|
|
"const": true,
|
|
"description": "Always true on success"
|
|
}
|
|
},
|
|
"required": [
|
|
"ok"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"404": {
|
|
"description": "404",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "NOT_FOUND"
|
|
},
|
|
"status": {
|
|
"const": 404
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Job not found"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "JOB_NOT_FOUND"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/cache/purge-metadata": {
|
|
"post": {
|
|
"operationId": "admin.purgeMetadataCache",
|
|
"summary": "Purge metadata cache",
|
|
"description": "Delete un-enriched stub titles not in any user's library and clean up orphaned person records.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Counts of deleted titles and persons",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"deletedTitles": {
|
|
"type": "number",
|
|
"description": "Number of un-enriched stub titles deleted"
|
|
},
|
|
"deletedPersons": {
|
|
"type": "number",
|
|
"description": "Number of orphaned person records deleted"
|
|
}
|
|
},
|
|
"required": [
|
|
"deletedTitles",
|
|
"deletedPersons"
|
|
],
|
|
"description": "Result of purging un-enriched metadata from the database"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/cache/purge-images": {
|
|
"post": {
|
|
"operationId": "admin.purgeImageCache",
|
|
"summary": "Purge image cache",
|
|
"description": "Delete all cached TMDB images from disk. Images will be re-downloaded on demand.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Count of deleted files and bytes freed",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"deletedFiles": {
|
|
"type": "number",
|
|
"description": "Number of image files deleted from disk"
|
|
},
|
|
"freedBytes": {
|
|
"type": "number",
|
|
"description": "Total bytes freed from disk"
|
|
}
|
|
},
|
|
"required": [
|
|
"deletedFiles",
|
|
"freedBytes"
|
|
],
|
|
"description": "Result of purging the image cache from disk"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/admin/system-health": {
|
|
"get": {
|
|
"operationId": "admin.systemHealth",
|
|
"summary": "Get system health report",
|
|
"description": "Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment.",
|
|
"tags": [
|
|
"Admin"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Full system health report",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/SystemHealth"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/account/name": {
|
|
"put": {
|
|
"operationId": "account.updateName",
|
|
"summary": "Update display name",
|
|
"description": "Change the current user's display name.",
|
|
"tags": [
|
|
"Account"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 100,
|
|
"description": "New display name"
|
|
}
|
|
},
|
|
"required": [
|
|
"name"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/account/avatar": {
|
|
"post": {
|
|
"operationId": "account.uploadAvatar",
|
|
"summary": "Upload avatar",
|
|
"description": "Upload a new profile avatar image. Accepts JPEG, PNG, WebP, or GIF up to 2 MB.",
|
|
"tags": [
|
|
"Account"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"multipart/form-data": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"file": {
|
|
"type": "string",
|
|
"format": "binary"
|
|
}
|
|
},
|
|
"required": [
|
|
"file"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "URL of the uploaded avatar image",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"imageUrl": {
|
|
"type": "string",
|
|
"description": "URL of the uploaded avatar image"
|
|
}
|
|
},
|
|
"required": [
|
|
"imageUrl"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
},
|
|
"delete": {
|
|
"operationId": "account.removeAvatar",
|
|
"summary": "Remove avatar",
|
|
"description": "Delete the current user's profile avatar, reverting to the default.",
|
|
"tags": [
|
|
"Account"
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK"
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/imports/parse-file": {
|
|
"post": {
|
|
"operationId": "imports.parseFile",
|
|
"summary": "Parse import file",
|
|
"description": "Upload and parse an export file from Trakt, Simkl, Letterboxd, or Sofa. Returns a preview of items found without importing anything.",
|
|
"tags": [
|
|
"Imports"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"source": {
|
|
"enum": [
|
|
"trakt",
|
|
"simkl",
|
|
"letterboxd",
|
|
"sofa"
|
|
],
|
|
"type": "string",
|
|
"description": "Service to import from"
|
|
},
|
|
"file": {
|
|
"type": "string",
|
|
"contentMediaType": "*/*"
|
|
}
|
|
},
|
|
"required": [
|
|
"source",
|
|
"file"
|
|
]
|
|
}
|
|
},
|
|
"multipart/form-data": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"source": {
|
|
"enum": [
|
|
"trakt",
|
|
"simkl",
|
|
"letterboxd",
|
|
"sofa"
|
|
],
|
|
"type": "string",
|
|
"description": "Service to import from"
|
|
},
|
|
"file": {
|
|
"type": "string",
|
|
"contentMediaType": "*/*"
|
|
}
|
|
},
|
|
"required": [
|
|
"source",
|
|
"file"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Preview of importable items with counts",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"source": {
|
|
"enum": [
|
|
"trakt",
|
|
"simkl",
|
|
"letterboxd",
|
|
"sofa"
|
|
],
|
|
"type": "string",
|
|
"description": "Service to import from"
|
|
},
|
|
"movies": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"title": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"watchedAt": {
|
|
"type": "string",
|
|
"format": "date-time",
|
|
"description": "ISO 8601 timestamp"
|
|
},
|
|
"watchedOn": {
|
|
"type": "string",
|
|
"format": "date",
|
|
"description": "YYYY-MM-DD date-only"
|
|
}
|
|
},
|
|
"required": [
|
|
"title"
|
|
]
|
|
}
|
|
},
|
|
"episodes": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"showTmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"tvdbId": {
|
|
"type": "number"
|
|
},
|
|
"showTitle": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"seasonNumber": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"maximum": 9007199254740991
|
|
},
|
|
"episodeNumber": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 9007199254740991
|
|
},
|
|
"watchedAt": {
|
|
"type": "string",
|
|
"format": "date-time"
|
|
},
|
|
"watchedOn": {
|
|
"type": "string",
|
|
"format": "date"
|
|
}
|
|
},
|
|
"required": [
|
|
"seasonNumber",
|
|
"episodeNumber"
|
|
]
|
|
}
|
|
},
|
|
"watchlist": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"tvdbId": {
|
|
"type": "number"
|
|
},
|
|
"title": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"enum": [
|
|
"watchlist",
|
|
"in_progress",
|
|
"completed"
|
|
],
|
|
"type": "string",
|
|
"description": "Library status (default: watchlist)"
|
|
},
|
|
"addedAt": {
|
|
"type": "string",
|
|
"format": "date-time",
|
|
"description": "When the item was added to library"
|
|
}
|
|
},
|
|
"required": [
|
|
"title",
|
|
"type"
|
|
]
|
|
}
|
|
},
|
|
"ratings": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"tvdbId": {
|
|
"type": "number"
|
|
},
|
|
"title": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"rating": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 5,
|
|
"description": "Sofa 1-5 star rating"
|
|
},
|
|
"ratedAt": {
|
|
"type": "string",
|
|
"format": "date-time"
|
|
},
|
|
"ratedOn": {
|
|
"type": "string",
|
|
"format": "date"
|
|
}
|
|
},
|
|
"required": [
|
|
"title",
|
|
"type",
|
|
"rating"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"source",
|
|
"movies",
|
|
"episodes",
|
|
"watchlist",
|
|
"ratings"
|
|
]
|
|
},
|
|
"warnings": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"stats": {
|
|
"type": "object",
|
|
"properties": {
|
|
"movies": {
|
|
"type": "number"
|
|
},
|
|
"episodes": {
|
|
"type": "number"
|
|
},
|
|
"watchlist": {
|
|
"type": "number"
|
|
},
|
|
"ratings": {
|
|
"type": "number"
|
|
}
|
|
},
|
|
"required": [
|
|
"movies",
|
|
"episodes",
|
|
"watchlist",
|
|
"ratings"
|
|
]
|
|
},
|
|
"diagnostics": {
|
|
"type": "object",
|
|
"properties": {
|
|
"unresolved": {
|
|
"type": "number"
|
|
},
|
|
"unsupported": {
|
|
"type": "number"
|
|
}
|
|
},
|
|
"required": [
|
|
"unresolved",
|
|
"unsupported"
|
|
]
|
|
},
|
|
"blockingErrors": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"data",
|
|
"warnings",
|
|
"stats"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"400": {
|
|
"description": "400",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "BAD_REQUEST"
|
|
},
|
|
"status": {
|
|
"const": 400
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Invalid import file"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "IMPORT_INVALID_FILE"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/imports/parse-payload": {
|
|
"post": {
|
|
"operationId": "imports.parsePayload",
|
|
"summary": "Parse raw OAuth import data",
|
|
"description": "Accept raw API responses from the OAuth proxy, parse them into normalized import format, and return a preview with item counts.",
|
|
"tags": [
|
|
"Imports"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"source": {
|
|
"enum": [
|
|
"trakt",
|
|
"simkl"
|
|
],
|
|
"type": "string",
|
|
"description": "OAuth import provider"
|
|
},
|
|
"rawPayload": {
|
|
"description": "Raw aggregated API response from the OAuth proxy"
|
|
}
|
|
},
|
|
"required": [
|
|
"source"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Preview of importable items with counts",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"source": {
|
|
"enum": [
|
|
"trakt",
|
|
"simkl",
|
|
"letterboxd",
|
|
"sofa"
|
|
],
|
|
"type": "string",
|
|
"description": "Service to import from"
|
|
},
|
|
"movies": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"title": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"watchedAt": {
|
|
"type": "string",
|
|
"format": "date-time",
|
|
"description": "ISO 8601 timestamp"
|
|
},
|
|
"watchedOn": {
|
|
"type": "string",
|
|
"format": "date",
|
|
"description": "YYYY-MM-DD date-only"
|
|
}
|
|
},
|
|
"required": [
|
|
"title"
|
|
]
|
|
}
|
|
},
|
|
"episodes": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"showTmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"tvdbId": {
|
|
"type": "number"
|
|
},
|
|
"showTitle": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"seasonNumber": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"maximum": 9007199254740991
|
|
},
|
|
"episodeNumber": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 9007199254740991
|
|
},
|
|
"watchedAt": {
|
|
"type": "string",
|
|
"format": "date-time"
|
|
},
|
|
"watchedOn": {
|
|
"type": "string",
|
|
"format": "date"
|
|
}
|
|
},
|
|
"required": [
|
|
"seasonNumber",
|
|
"episodeNumber"
|
|
]
|
|
}
|
|
},
|
|
"watchlist": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"tvdbId": {
|
|
"type": "number"
|
|
},
|
|
"title": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"enum": [
|
|
"watchlist",
|
|
"in_progress",
|
|
"completed"
|
|
],
|
|
"type": "string",
|
|
"description": "Library status (default: watchlist)"
|
|
},
|
|
"addedAt": {
|
|
"type": "string",
|
|
"format": "date-time",
|
|
"description": "When the item was added to library"
|
|
}
|
|
},
|
|
"required": [
|
|
"title",
|
|
"type"
|
|
]
|
|
}
|
|
},
|
|
"ratings": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"tvdbId": {
|
|
"type": "number"
|
|
},
|
|
"title": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"rating": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 5,
|
|
"description": "Sofa 1-5 star rating"
|
|
},
|
|
"ratedAt": {
|
|
"type": "string",
|
|
"format": "date-time"
|
|
},
|
|
"ratedOn": {
|
|
"type": "string",
|
|
"format": "date"
|
|
}
|
|
},
|
|
"required": [
|
|
"title",
|
|
"type",
|
|
"rating"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"source",
|
|
"movies",
|
|
"episodes",
|
|
"watchlist",
|
|
"ratings"
|
|
]
|
|
},
|
|
"warnings": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"stats": {
|
|
"type": "object",
|
|
"properties": {
|
|
"movies": {
|
|
"type": "number"
|
|
},
|
|
"episodes": {
|
|
"type": "number"
|
|
},
|
|
"watchlist": {
|
|
"type": "number"
|
|
},
|
|
"ratings": {
|
|
"type": "number"
|
|
}
|
|
},
|
|
"required": [
|
|
"movies",
|
|
"episodes",
|
|
"watchlist",
|
|
"ratings"
|
|
]
|
|
},
|
|
"diagnostics": {
|
|
"type": "object",
|
|
"properties": {
|
|
"unresolved": {
|
|
"type": "number"
|
|
},
|
|
"unsupported": {
|
|
"type": "number"
|
|
}
|
|
},
|
|
"required": [
|
|
"unresolved",
|
|
"unsupported"
|
|
]
|
|
},
|
|
"blockingErrors": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"data",
|
|
"warnings",
|
|
"stats"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/imports/jobs": {
|
|
"post": {
|
|
"operationId": "imports.createJob",
|
|
"summary": "Create import job",
|
|
"description": "Create and start a background import job from previously parsed data.",
|
|
"tags": [
|
|
"Imports"
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"source": {
|
|
"enum": [
|
|
"trakt",
|
|
"simkl",
|
|
"letterboxd",
|
|
"sofa"
|
|
],
|
|
"type": "string",
|
|
"description": "Service to import from"
|
|
},
|
|
"movies": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"title": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"watchedAt": {
|
|
"type": "string",
|
|
"format": "date-time",
|
|
"description": "ISO 8601 timestamp"
|
|
},
|
|
"watchedOn": {
|
|
"type": "string",
|
|
"format": "date",
|
|
"description": "YYYY-MM-DD date-only"
|
|
}
|
|
},
|
|
"required": [
|
|
"title"
|
|
]
|
|
}
|
|
},
|
|
"episodes": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"showTmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"tvdbId": {
|
|
"type": "number"
|
|
},
|
|
"showTitle": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"seasonNumber": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"maximum": 9007199254740991
|
|
},
|
|
"episodeNumber": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 9007199254740991
|
|
},
|
|
"watchedAt": {
|
|
"type": "string",
|
|
"format": "date-time"
|
|
},
|
|
"watchedOn": {
|
|
"type": "string",
|
|
"format": "date"
|
|
}
|
|
},
|
|
"required": [
|
|
"seasonNumber",
|
|
"episodeNumber"
|
|
]
|
|
}
|
|
},
|
|
"watchlist": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"tvdbId": {
|
|
"type": "number"
|
|
},
|
|
"title": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"enum": [
|
|
"watchlist",
|
|
"in_progress",
|
|
"completed"
|
|
],
|
|
"type": "string",
|
|
"description": "Library status (default: watchlist)"
|
|
},
|
|
"addedAt": {
|
|
"type": "string",
|
|
"format": "date-time",
|
|
"description": "When the item was added to library"
|
|
}
|
|
},
|
|
"required": [
|
|
"title",
|
|
"type"
|
|
]
|
|
}
|
|
},
|
|
"ratings": {
|
|
"type": "array",
|
|
"maxItems": 50000,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tmdbId": {
|
|
"type": "number"
|
|
},
|
|
"imdbId": {
|
|
"type": "string"
|
|
},
|
|
"tvdbId": {
|
|
"type": "number"
|
|
},
|
|
"title": {
|
|
"type": "string"
|
|
},
|
|
"year": {
|
|
"type": "number"
|
|
},
|
|
"type": {
|
|
"enum": [
|
|
"movie",
|
|
"tv"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"rating": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 5,
|
|
"description": "Sofa 1-5 star rating"
|
|
},
|
|
"ratedAt": {
|
|
"type": "string",
|
|
"format": "date-time"
|
|
},
|
|
"ratedOn": {
|
|
"type": "string",
|
|
"format": "date"
|
|
}
|
|
},
|
|
"required": [
|
|
"title",
|
|
"type",
|
|
"rating"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"required": [
|
|
"source",
|
|
"movies",
|
|
"episodes",
|
|
"watchlist",
|
|
"ratings"
|
|
]
|
|
},
|
|
"options": {
|
|
"type": "object",
|
|
"properties": {
|
|
"importWatches": {
|
|
"type": "boolean",
|
|
"description": "Import movie and episode watch history"
|
|
},
|
|
"importWatchlist": {
|
|
"type": "boolean",
|
|
"description": "Import watchlist items"
|
|
},
|
|
"importRatings": {
|
|
"type": "boolean",
|
|
"description": "Import ratings"
|
|
}
|
|
},
|
|
"required": [
|
|
"importWatches",
|
|
"importWatchlist",
|
|
"importRatings"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"data",
|
|
"options"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Created import job",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string"
|
|
},
|
|
"source": {
|
|
"enum": [
|
|
"trakt",
|
|
"simkl",
|
|
"letterboxd",
|
|
"sofa"
|
|
],
|
|
"type": "string",
|
|
"description": "Service to import from"
|
|
},
|
|
"status": {
|
|
"enum": [
|
|
"pending",
|
|
"running",
|
|
"success",
|
|
"error",
|
|
"cancelled"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"totalItems": {
|
|
"type": "number"
|
|
},
|
|
"processedItems": {
|
|
"type": "number"
|
|
},
|
|
"importedCount": {
|
|
"type": "number"
|
|
},
|
|
"skippedCount": {
|
|
"type": "number"
|
|
},
|
|
"failedCount": {
|
|
"type": "number"
|
|
},
|
|
"currentMessage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"errors": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"warnings": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"createdAt": {
|
|
"type": "string"
|
|
},
|
|
"startedAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"finishedAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"source",
|
|
"status",
|
|
"totalItems",
|
|
"processedItems",
|
|
"importedCount",
|
|
"skippedCount",
|
|
"failedCount",
|
|
"currentMessage",
|
|
"errors",
|
|
"warnings",
|
|
"createdAt",
|
|
"startedAt",
|
|
"finishedAt"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"400": {
|
|
"description": "400",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "BAD_REQUEST"
|
|
},
|
|
"status": {
|
|
"const": 400
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Import payload too large"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "IMPORT_PAYLOAD_TOO_LARGE"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"409": {
|
|
"description": "409",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "CONFLICT"
|
|
},
|
|
"status": {
|
|
"const": 409
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "An import is already in progress"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "IMPORT_ALREADY_RUNNING"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/imports/jobs/{id}": {
|
|
"get": {
|
|
"operationId": "imports.getJob",
|
|
"summary": "Get import job status",
|
|
"description": "Get the current status and progress of an import job.",
|
|
"tags": [
|
|
"Imports"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Current job state",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string"
|
|
},
|
|
"source": {
|
|
"enum": [
|
|
"trakt",
|
|
"simkl",
|
|
"letterboxd",
|
|
"sofa"
|
|
],
|
|
"type": "string",
|
|
"description": "Service to import from"
|
|
},
|
|
"status": {
|
|
"enum": [
|
|
"pending",
|
|
"running",
|
|
"success",
|
|
"error",
|
|
"cancelled"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"totalItems": {
|
|
"type": "number"
|
|
},
|
|
"processedItems": {
|
|
"type": "number"
|
|
},
|
|
"importedCount": {
|
|
"type": "number"
|
|
},
|
|
"skippedCount": {
|
|
"type": "number"
|
|
},
|
|
"failedCount": {
|
|
"type": "number"
|
|
},
|
|
"currentMessage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"errors": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"warnings": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"createdAt": {
|
|
"type": "string"
|
|
},
|
|
"startedAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"finishedAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"source",
|
|
"status",
|
|
"totalItems",
|
|
"processedItems",
|
|
"importedCount",
|
|
"skippedCount",
|
|
"failedCount",
|
|
"currentMessage",
|
|
"errors",
|
|
"warnings",
|
|
"createdAt",
|
|
"startedAt",
|
|
"finishedAt"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/imports/jobs/{id}/cancel": {
|
|
"post": {
|
|
"operationId": "imports.cancelJob",
|
|
"summary": "Cancel import job",
|
|
"description": "Cancel a pending or running import job. Already-imported items are not rolled back.",
|
|
"tags": [
|
|
"Imports"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": false,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Updated job state",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string"
|
|
},
|
|
"source": {
|
|
"enum": [
|
|
"trakt",
|
|
"simkl",
|
|
"letterboxd",
|
|
"sofa"
|
|
],
|
|
"type": "string",
|
|
"description": "Service to import from"
|
|
},
|
|
"status": {
|
|
"enum": [
|
|
"pending",
|
|
"running",
|
|
"success",
|
|
"error",
|
|
"cancelled"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"totalItems": {
|
|
"type": "number"
|
|
},
|
|
"processedItems": {
|
|
"type": "number"
|
|
},
|
|
"importedCount": {
|
|
"type": "number"
|
|
},
|
|
"skippedCount": {
|
|
"type": "number"
|
|
},
|
|
"failedCount": {
|
|
"type": "number"
|
|
},
|
|
"currentMessage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"errors": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"warnings": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"createdAt": {
|
|
"type": "string"
|
|
},
|
|
"startedAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"finishedAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"source",
|
|
"status",
|
|
"totalItems",
|
|
"processedItems",
|
|
"importedCount",
|
|
"skippedCount",
|
|
"failedCount",
|
|
"currentMessage",
|
|
"errors",
|
|
"warnings",
|
|
"createdAt",
|
|
"startedAt",
|
|
"finishedAt"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"400": {
|
|
"description": "400",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": true
|
|
},
|
|
"code": {
|
|
"const": "BAD_REQUEST"
|
|
},
|
|
"status": {
|
|
"const": 400
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"default": "Import cannot be cancelled"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": {
|
|
"const": "IMPORT_CANNOT_CANCEL"
|
|
}
|
|
},
|
|
"required": [
|
|
"code"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"defined": {
|
|
"const": false
|
|
},
|
|
"code": {
|
|
"type": "string"
|
|
},
|
|
"status": {
|
|
"type": "number"
|
|
},
|
|
"message": {
|
|
"type": "string"
|
|
},
|
|
"data": {}
|
|
},
|
|
"required": [
|
|
"defined",
|
|
"code",
|
|
"status",
|
|
"message"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/imports/jobs/{id}/events": {
|
|
"get": {
|
|
"operationId": "imports.jobEvents",
|
|
"summary": "Stream import job events",
|
|
"description": "SSE stream of import job progress events. Yields progress updates and a final complete event.",
|
|
"tags": [
|
|
"Imports"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Internal UUIDv7 identifier"
|
|
}
|
|
}
|
|
],
|
|
"responses": {
|
|
"200": {
|
|
"description": "Stream of job progress events",
|
|
"content": {
|
|
"text/event-stream": {
|
|
"schema": {
|
|
"oneOf": [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"event": {
|
|
"const": "message"
|
|
},
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": {
|
|
"enum": [
|
|
"progress",
|
|
"complete",
|
|
"timeout"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"job": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string"
|
|
},
|
|
"source": {
|
|
"enum": [
|
|
"trakt",
|
|
"simkl",
|
|
"letterboxd",
|
|
"sofa"
|
|
],
|
|
"type": "string",
|
|
"description": "Service to import from"
|
|
},
|
|
"status": {
|
|
"enum": [
|
|
"pending",
|
|
"running",
|
|
"success",
|
|
"error",
|
|
"cancelled"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"totalItems": {
|
|
"type": "number"
|
|
},
|
|
"processedItems": {
|
|
"type": "number"
|
|
},
|
|
"importedCount": {
|
|
"type": "number"
|
|
},
|
|
"skippedCount": {
|
|
"type": "number"
|
|
},
|
|
"failedCount": {
|
|
"type": "number"
|
|
},
|
|
"currentMessage": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"errors": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"warnings": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"createdAt": {
|
|
"type": "string"
|
|
},
|
|
"startedAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
},
|
|
"finishedAt": {
|
|
"anyOf": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"type": "null"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"source",
|
|
"status",
|
|
"totalItems",
|
|
"processedItems",
|
|
"importedCount",
|
|
"skippedCount",
|
|
"failedCount",
|
|
"currentMessage",
|
|
"errors",
|
|
"warnings",
|
|
"createdAt",
|
|
"startedAt",
|
|
"finishedAt"
|
|
]
|
|
}
|
|
},
|
|
"required": [
|
|
"type",
|
|
"job"
|
|
]
|
|
},
|
|
"id": {
|
|
"type": "string"
|
|
},
|
|
"retry": {
|
|
"type": "number"
|
|
}
|
|
},
|
|
"required": [
|
|
"event",
|
|
"data"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"event": {
|
|
"const": "done"
|
|
},
|
|
"data": {},
|
|
"id": {
|
|
"type": "string"
|
|
},
|
|
"retry": {
|
|
"type": "number"
|
|
}
|
|
},
|
|
"required": [
|
|
"event"
|
|
]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"event": {
|
|
"const": "error"
|
|
},
|
|
"data": {},
|
|
"id": {
|
|
"type": "string"
|
|
},
|
|
"retry": {
|
|
"type": "number"
|
|
}
|
|
},
|
|
"required": [
|
|
"event"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"security": [
|
|
{
|
|
"session": []
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
} |