Implement full Couch Potato movie & TV tracking app

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>
This commit is contained in:
2026-02-27 14:42:13 -05:00
co-authored by Claude Opus 4.6
parent 1b0ca5431a
commit b02ff1cdc1
65 changed files with 4994 additions and 382 deletions
+96
View File
@@ -0,0 +1,96 @@
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}`;
}
+112
View File
@@ -0,0 +1,112 @@
export interface TmdbSearchResult {
id: number;
media_type: "movie" | "tv" | "person";
title?: string;
name?: string;
original_title?: string;
original_name?: string;
overview: string;
release_date?: string;
first_air_date?: string;
poster_path: string | null;
backdrop_path: string | null;
popularity: number;
vote_average: number;
vote_count: number;
}
export interface TmdbSearchResponse {
page: number;
results: TmdbSearchResult[];
total_pages: number;
total_results: number;
}
export interface TmdbMovieDetails {
id: number;
title: string;
original_title: string;
overview: string;
release_date: string;
poster_path: string | null;
backdrop_path: string | null;
popularity: number;
vote_average: number;
vote_count: number;
status: string;
}
export interface TmdbTvDetails {
id: number;
name: string;
original_name: string;
overview: string;
first_air_date: string;
poster_path: string | null;
backdrop_path: string | null;
popularity: number;
vote_average: number;
vote_count: number;
status: string;
number_of_seasons: number;
seasons: TmdbSeasonSummary[];
}
export interface TmdbSeasonSummary {
id: number;
season_number: number;
name: string;
overview: string;
poster_path: string | null;
air_date: string | null;
episode_count: number;
}
export interface TmdbSeasonDetails {
id: number;
season_number: number;
name: string;
overview: string;
poster_path: string | null;
air_date: string | null;
episodes: TmdbEpisode[];
}
export interface TmdbEpisode {
id: number;
episode_number: number;
name: string;
overview: string;
still_path: string | null;
air_date: string | null;
runtime: number | null;
}
export interface TmdbWatchProviderResponse {
id: number;
results: Record<
string,
{
link?: string;
flatrate?: TmdbProvider[];
rent?: TmdbProvider[];
buy?: TmdbProvider[];
free?: TmdbProvider[];
ads?: TmdbProvider[];
}
>;
}
export interface TmdbProvider {
provider_id: number;
provider_name: string;
logo_path: string;
display_priority: number;
}
export interface TmdbRecommendationResponse {
page: number;
results: TmdbSearchResult[];
total_pages: number;
total_results: number;
}