feat(content): add TV Series content type with TMDB integration

Add complete TV series content surface: extraction from AI responses,
grid/detail views, TMDB TV search endpoint, and panel tab integration.
Includes film_ext-to-TV-series conversion for backward compatibility
and AI prompt instructions for [[tv_ext:...]] tags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-03 06:50:03 +00:00
co-authored by Claude Opus 4.6
parent 99e380e1b2
commit ad63a7b1af
12 changed files with 870 additions and 26 deletions
@@ -120,11 +120,12 @@ function escapeXml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
}
export async function fetchTmdbPoster(
async function fetchTmdbGeneric(
endpoint: string,
title: string,
year?: number,
): Promise<TmdbResult> {
const key = cacheKey(title, year)
const key = `${endpoint}:${cacheKey(title, year)}`
const cached = memoryCache.get(key)
if (cached) return cached
@@ -132,7 +133,7 @@ export async function fetchTmdbPoster(
try {
const params = new URLSearchParams({ q: title.trim() })
if (year && year > 0) params.set('y', String(year))
const res = await fetch(`/api/tmdb/search?${params}`)
const res = await fetch(`/api/tmdb/${endpoint}?${params}`)
if (!res.ok) return empty
const data = (await res.json()) as { posterUrl?: string | null; backdropUrl?: string | null }
const result: TmdbResult = {
@@ -149,6 +150,13 @@ export async function fetchTmdbPoster(
}
}
export async function fetchTmdbPoster(
title: string,
year?: number,
): Promise<TmdbResult> {
return fetchTmdbGeneric('search', title, year)
}
export async function handleImgError(
e: Event,
title: string,
@@ -185,6 +193,13 @@ export function isUrlFailed(url: string | undefined): boolean {
return !!url && failedUrls.has(url)
}
export async function fetchTmdbTVPoster(
title: string,
year?: number,
): Promise<TmdbResult> {
return fetchTmdbGeneric('search-tv', title, year)
}
/** Fetch podcast artwork from iTunes Search API (free, no key). Returns hi-res URL (600x600). */
export async function fetchPodcastCover(
title: string,