Files
sofa/lib/providers.ts
T
jake cd4b2908d8 Add clickable watch links to streaming provider badges
Build a provider registry mapping TMDB provider IDs to search URL
templates. `generateProviderUrl()` resolves a URL for a given provider
and title name using URL-encoded search queries. `readAvailability()`
now accepts the title name and attaches `watchUrl` to each offer.

Provider badges with a resolved URL render as `<a>` links opening in a
new tab; tooltip copy switches from the provider name to "Watch on
{name}". Badges without a known URL remain non-interactive as before.
2026-03-05 20:35:59 -05:00

121 lines
3.0 KiB
TypeScript

/**
* Provider registry mapping TMDB provider IDs to search URL templates.
*
* To add a new provider:
* 1. Find the TMDB provider_id (visible in availability data or TMDB API)
* 2. Add an entry below with the service's search URL using {title} placeholder
*/
interface ProviderConfig {
name: string;
searchUrl: string;
}
// TMDB provider_id → search URL config
const providers: Record<number, ProviderConfig> = {
// Netflix
8: { name: "Netflix", searchUrl: "https://www.netflix.com/search?q={title}" },
1796: {
name: "Netflix basic with Ads",
searchUrl: "https://www.netflix.com/search?q={title}",
},
// Amazon
9: {
name: "Amazon Prime Video",
searchUrl: "https://www.amazon.com/s?i=instant-video&k={title}",
},
10: {
name: "Amazon Video",
searchUrl: "https://www.amazon.com/s?i=instant-video&k={title}",
},
119: {
name: "Amazon Prime Video",
searchUrl: "https://www.amazon.com/s?i=instant-video&k={title}",
},
// Disney+
337: {
name: "Disney+",
searchUrl: "https://www.disneyplus.com/search/{title}",
},
// Apple
2: {
name: "Apple iTunes",
searchUrl: "https://tv.apple.com/search?term={title}",
},
350: {
name: "Apple TV+",
searchUrl: "https://tv.apple.com/search?term={title}",
},
// Hulu
15: { name: "Hulu", searchUrl: "https://www.hulu.com/search?q={title}" },
// Max (HBO)
384: { name: "HBO Max", searchUrl: "https://play.max.com/search?q={title}" },
1899: { name: "Max", searchUrl: "https://play.max.com/search?q={title}" },
// Paramount+
531: {
name: "Paramount+",
searchUrl: "https://www.paramountplus.com/search/?q={title}",
},
// Peacock
386: {
name: "Peacock",
searchUrl: "https://www.peacocktv.com/search?q={title}",
},
// Google Play
3: {
name: "Google Play Movies",
searchUrl: "https://play.google.com/store/search?q={title}&c=movies",
},
// YouTube
192: {
name: "YouTube",
searchUrl: "https://www.youtube.com/results?search_query={title}",
},
// Crunchyroll
283: {
name: "Crunchyroll",
searchUrl: "https://www.crunchyroll.com/search?q={title}",
},
// Free / ad-supported
73: { name: "Tubi", searchUrl: "https://tubitv.com/search/{title}" },
300: {
name: "Pluto TV",
searchUrl: "https://pluto.tv/search/details?q={title}",
},
// Other
257: { name: "fuboTV", searchUrl: "https://www.fubo.tv/search/{title}" },
43: {
name: "Starz",
searchUrl: "https://www.starz.com/search?query={title}",
},
37: {
name: "Showtime",
searchUrl: "https://www.sho.com/search?q={title}",
},
};
const providerRegistry: ReadonlyMap<number, ProviderConfig> = new Map(
Object.entries(providers).map(([id, config]) => [Number(id), config]),
);
export function generateProviderUrl(
providerId: number,
titleName: string,
): string | null {
const config = providerRegistry.get(providerId);
if (!config) return null;
return config.searchUrl.replace("{title}", encodeURIComponent(titleName));
}