refactor: return pre-resolved internal IDs from all listing endpoints and remove client-side resolve mutations

All explore, discover, search, recommendation, and person-credit listing procedures now include the internal database `id` on every item so clients can navigate and act without a separate resolve round-trip.

- Remove `titles.resolve` and `people.resolve` mutation calls from the native search screen, hero banners, poster rows, and cast cards; replace with direct `Link` navigation using the pre-returned `id`.
- Change `titles.quickAdd` to accept `{ id }` instead of `{ tmdbId, type }` and update every call site on native and web.
- Key all user-status and episode-progress lookups by `id` instead of `tmdbId-type` composite strings across `PosterCard`, `HorizontalPosterRow`, `FilterableTitleRow`, and `usePosterActions`; drop the `tmdbId` prop from `PosterCard` entirely.
- Remove the `titles.hydrateSeasons` auto-trigger from the title detail screen; season hydration now happens server-side on resolve.
- Delete the `browse-thumbhashes` and `browse-title-ids` server procedures and remove them from the router.
- Extend `@sofa/api` schemas with an `id` field on all listing-item types; update `packages/core` services and add a DB migration accordingly.
This commit is contained in:
2026-03-16 16:35:50 -04:00
parent ccc7922737
commit 25736f684a
48 changed files with 6965 additions and 1777 deletions
@@ -1,19 +0,0 @@
---
title: Resolve TMDB ID to local person
full: true
_openapi:
method: POST
toc: []
structuredData:
headings: []
contents:
- content: >-
Look up or import a person by their TMDB ID. Returns the internal ID
for use with other endpoints.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Look up or import a person by their TMDB ID. Returns the internal ID for use with other endpoints.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/people/resolve","method":"post"}]} />
@@ -9,12 +9,11 @@ _openapi:
contents:
- content: >-
Comprehensive health check covering database, TMDB connectivity, cron
jobs, image cache, backups, and environment. Does not require
authentication.
jobs, image cache, backups, and environment. Admin only.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment. Does not require authentication.
Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment. Admin only.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/system/health","method":"get"}]} />
@@ -1,19 +0,0 @@
---
title: Hydrate TV seasons
full: true
_openapi:
method: POST
toc: []
structuredData:
headings: []
contents:
- content: >-
Fetch full season and episode data from TMDB for a TV show. Required
before tracking individual episodes.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Fetch full season and episode data from TMDB for a TV show. Required before tracking individual episodes.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/titles/{id}/hydrate-seasons","method":"post"}]} />
@@ -8,13 +8,13 @@ _openapi:
headings: []
contents:
- content: >-
Import a title by TMDB ID and add it to the user's watchlist in one
step. If the title already exists in the user's library, returns
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.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Import a title by TMDB ID and add it to the user's watchlist in one step. If the title already exists in the user's library, returns alreadyAdded: true.
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.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/titles/quick-add","method":"post"}]} />
<APIPage document={"./public/openapi.json"} operations={[{"path":"/titles/{id}/quick-add","method":"post"}]} />
@@ -1,19 +0,0 @@
---
title: Resolve TMDB ID to local title
full: true
_openapi:
method: POST
toc: []
structuredData:
headings: []
contents:
- content: >-
Look up or import a title by its TMDB ID and media type. Returns the
internal ID for use with other endpoints.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Look up or import a title by its TMDB ID and media type. Returns the internal ID for use with other endpoints.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/titles/resolve","method":"post"}]} />
+76 -346
View File
@@ -248,6 +248,50 @@
],
"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": [
{
@@ -366,6 +410,10 @@
"voteCount",
"status",
"contentRating",
"imdbId",
"tvdbId",
"originalLanguage",
"runtimeMinutes",
"colorPalette",
"trailerVideoKey",
"genres"
@@ -840,6 +888,10 @@
"BrowseItem": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Internal title ID"
},
"tmdbId": {
"type": "number",
"description": "TMDB numeric ID"
@@ -912,6 +964,7 @@
}
},
"required": [
"id",
"tmdbId",
"type",
"title",
@@ -1544,11 +1597,7 @@
"items": {
"$ref": "#/components/schemas/Season"
},
"description": "TV seasons (empty for movies or unhydrated shows)"
},
"needsHydration": {
"type": "boolean",
"description": "Whether season/episode data needs to be fetched from TMDB before tracking"
"description": "TV seasons (empty for movies)"
},
"availability": {
"type": "array",
@@ -1612,7 +1661,6 @@
"required": [
"title",
"seasons",
"needsHydration",
"availability",
"cast"
],
@@ -1689,133 +1737,6 @@
]
}
},
"/titles/resolve": {
"post": {
"operationId": "titles.resolve",
"summary": "Resolve TMDB ID to local title",
"description": "Look up or import a title by its TMDB ID and media type. Returns the internal ID for use with other endpoints.",
"tags": [
"Titles"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tmdbId": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "TMDB numeric ID"
},
"type": {
"enum": [
"movie",
"tv"
],
"description": "Media type"
}
},
"required": [
"tmdbId",
"type"
],
"description": "TMDB ID and media type pair for resolving titles"
}
}
}
},
"responses": {
"200": {
"description": "Internal title ID",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Internal title ID"
}
},
"required": [
"id"
],
"description": "Resolved internal title ID"
}
}
}
},
"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": {}
},
"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": []
}
]
}
},
"/titles/{id}/status": {
"put": {
"operationId": "titles.updateStatus",
@@ -2162,11 +2083,11 @@
]
}
},
"/titles/{id}/hydrate-seasons": {
"/titles/{id}/quick-add": {
"post": {
"operationId": "titles.hydrateSeasons",
"summary": "Hydrate TV seasons",
"description": "Fetch full season and episode data from TMDB for a TV show. Required before tracking individual episodes.",
"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"
],
@@ -2178,98 +2099,16 @@
"schema": {
"type": "string",
"minLength": 1,
"description": "Internal title ID"
"description": "Internal UUIDv7 identifier"
}
}
],
"requestBody": {
"required": true,
"required": false,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tmdbId": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "TMDB ID for fetching season data"
}
},
"required": [
"tmdbId"
],
"description": "Title identifiers for fetching season/episode data"
}
}
}
},
"responses": {
"200": {
"description": "Hydrated seasons with episodes",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"seasons": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Season"
},
"description": "Hydrated seasons with episodes"
}
},
"required": [
"seasons"
],
"description": "Freshly fetched season and episode data from TMDB"
}
}
}
}
},
"security": [
{
"session": []
}
]
}
},
"/titles/quick-add": {
"post": {
"operationId": "titles.quickAdd",
"summary": "Quick add title to library",
"description": "Import a title by TMDB ID and add it to the user's watchlist in one step. If the title already exists in the user's library, returns alreadyAdded: true.",
"tags": [
"Titles"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tmdbId": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "TMDB numeric ID"
},
"type": {
"enum": [
"movie",
"tv"
],
"description": "Media type"
}
},
"required": [
"tmdbId",
"type"
],
"description": "TMDB ID and media type pair for resolving titles"
"type": "object"
}
}
}
@@ -2300,8 +2139,8 @@
}
}
},
"500": {
"description": "500",
"404": {
"description": "404",
"content": {
"application/json": {
"schema": {
@@ -2313,14 +2152,14 @@
"const": true
},
"code": {
"const": "INTERNAL_SERVER_ERROR"
"const": "NOT_FOUND"
},
"status": {
"const": 500
"const": 404
},
"message": {
"type": "string",
"default": "Failed to import title from TMDB"
"default": "Title not found"
},
"data": {}
},
@@ -2754,124 +2593,6 @@
]
}
},
"/people/resolve": {
"post": {
"operationId": "people.resolve",
"summary": "Resolve TMDB ID to local person",
"description": "Look up or import a person by their TMDB ID. Returns the internal ID for use with other endpoints.",
"tags": [
"People"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tmdbId": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "The Movie Database (TMDB) numeric ID"
}
},
"required": [
"tmdbId"
]
}
}
}
},
"responses": {
"200": {
"description": "Internal person ID",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Internal person ID"
}
},
"required": [
"id"
],
"description": "Resolved internal person ID"
}
}
}
},
"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": {}
},
"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": []
}
]
}
},
"/dashboard/stats": {
"get": {
"operationId": "dashboard.stats",
@@ -3464,6 +3185,10 @@
{
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Internal title ID"
},
"tmdbId": {
"type": "number",
"description": "TMDB numeric ID"
@@ -3500,6 +3225,7 @@
}
},
"required": [
"id",
"tmdbId",
"type",
"title",
@@ -4027,6 +3753,10 @@
"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"
@@ -4595,7 +4325,7 @@
"get": {
"operationId": "system.health",
"summary": "Get system health report",
"description": "Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment. Does not require authentication.",
"description": "Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment. Admin only.",
"tags": [
"System"
],