mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
- Drop `@sofa/core` and `@sofa/api` from `apps/public-api` — importing workspace packages with DB/Node dependencies broke the Vercel edge runtime - `fetchUserData` on Trakt and Simkl importers now returns raw `unknown` API responses; parsing is deferred to the self-hosted server - Add `source` + `rawPayload` fields to `ParsePayloadInput`; `parsePayload` procedure now dispatches to `parseTraktPayload`/`parseSimklPayload` and returns full warnings + diagnostics - Export `ProviderEnum` from `apps/public-api/src/importers/index.ts` instead of defining it inline in `app.ts`
7967 lines
246 KiB
JSON
7967 lines
246 KiB
JSON
{
|
|
"info": {
|
|
"title": "Sofa API",
|
|
"version": "0.1.0"
|
|
},
|
|
"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": {
|
|
"/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
|
|
}
|
|
],
|
|
"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/library": {
|
|
"get": {
|
|
"operationId": "dashboard.library",
|
|
"summary": "Get user library",
|
|
"description": "Fetch paginated titles in the user's library with their tracking statuses.",
|
|
"tags": [
|
|
"Dashboard"
|
|
],
|
|
"parameters": [
|
|
{
|
|
"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": "Paginated library items with user statuses",
|
|
"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"
|
|
}
|
|
},
|
|
"required": [
|
|
"id",
|
|
"tmdbId",
|
|
"type",
|
|
"title",
|
|
"posterPath",
|
|
"posterThumbHash",
|
|
"releaseDate",
|
|
"firstAirDate",
|
|
"voteAverage",
|
|
"userStatus"
|
|
],
|
|
"description": "A library item with user 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": [
|
|
"items",
|
|
"page",
|
|
"totalPages",
|
|
"totalResults"
|
|
],
|
|
"description": "Paginated titles in the user's library"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"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": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"/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": true,
|
|
"schema": {
|
|
"type": "integer",
|
|
"minimum": -9007199254740991,
|
|
"maximum": 9007199254740991,
|
|
"description": "TMDB genre ID to filter by"
|
|
},
|
|
"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": []
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
} |