mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 06:15:39 -04:00
Add all 10 milestones: Drizzle ORM + SQLite database with WAL mode, Better Auth email/password authentication, TMDB API integration for search and metadata import, TV season/episode caching, user tracking (watchlist/status/watches/ratings with auto-transitions), discovery feeds (continue watching, library, recommendations), US streaming availability via TMDB providers, background job scheduler with instrumentation hook, and dark cinema-themed frontend with DM Serif Display + DM Sans typography and amber accent design system. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
97 lines
2.4 KiB
TypeScript
97 lines
2.4 KiB
TypeScript
import type {
|
|
TmdbMovieDetails,
|
|
TmdbRecommendationResponse,
|
|
TmdbSearchResponse,
|
|
TmdbSeasonDetails,
|
|
TmdbTvDetails,
|
|
TmdbWatchProviderResponse,
|
|
} from "./types";
|
|
|
|
const BASE_URL = "https://api.themoviedb.org/3";
|
|
|
|
function getApiKey() {
|
|
const key = process.env.TMDB_API_KEY;
|
|
if (!key) throw new Error("TMDB_API_KEY is not set");
|
|
return key;
|
|
}
|
|
|
|
async function tmdbFetch<T>(
|
|
path: string,
|
|
params?: Record<string, string>,
|
|
): Promise<T> {
|
|
const url = new URL(`${BASE_URL}${path}`);
|
|
if (params) {
|
|
for (const [k, v] of Object.entries(params)) {
|
|
url.searchParams.set(k, v);
|
|
}
|
|
}
|
|
|
|
const res = await fetch(url.toString(), {
|
|
headers: {
|
|
Authorization: `Bearer ${getApiKey()}`,
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`TMDB API error: ${res.status} ${res.statusText}`);
|
|
}
|
|
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
export async function searchMulti(query: string, page = 1) {
|
|
return tmdbFetch<TmdbSearchResponse>("/search/multi", {
|
|
query,
|
|
page: String(page),
|
|
include_adult: "false",
|
|
});
|
|
}
|
|
|
|
export async function searchMovies(query: string, page = 1) {
|
|
return tmdbFetch<TmdbSearchResponse>("/search/movie", {
|
|
query,
|
|
page: String(page),
|
|
});
|
|
}
|
|
|
|
export async function searchTv(query: string, page = 1) {
|
|
return tmdbFetch<TmdbSearchResponse>("/search/tv", {
|
|
query,
|
|
page: String(page),
|
|
});
|
|
}
|
|
|
|
export async function getMovieDetails(tmdbId: number) {
|
|
return tmdbFetch<TmdbMovieDetails>(`/movie/${tmdbId}`);
|
|
}
|
|
|
|
export async function getTvDetails(tmdbId: number) {
|
|
return tmdbFetch<TmdbTvDetails>(`/tv/${tmdbId}`);
|
|
}
|
|
|
|
export async function getTvSeasonDetails(tmdbId: number, seasonNumber: number) {
|
|
return tmdbFetch<TmdbSeasonDetails>(`/tv/${tmdbId}/season/${seasonNumber}`);
|
|
}
|
|
|
|
export async function getWatchProviders(tmdbId: number, type: "movie" | "tv") {
|
|
return tmdbFetch<TmdbWatchProviderResponse>(
|
|
`/${type}/${tmdbId}/watch/providers`,
|
|
);
|
|
}
|
|
|
|
export async function getRecommendations(tmdbId: number, type: "movie" | "tv") {
|
|
return tmdbFetch<TmdbRecommendationResponse>(
|
|
`/${type}/${tmdbId}/recommendations`,
|
|
);
|
|
}
|
|
|
|
export async function getSimilar(tmdbId: number, type: "movie" | "tv") {
|
|
return tmdbFetch<TmdbRecommendationResponse>(`/${type}/${tmdbId}/similar`);
|
|
}
|
|
|
|
export function tmdbImageUrl(path: string | null, size = "w500") {
|
|
if (!path) return null;
|
|
return `https://image.tmdb.org/t/p/${size}${path}`;
|
|
}
|