Files
sofa/scripts/sync-tmdb-providers.ts
T
jake b0172a2b25 feat: refactor platforms to support multiple TMDB provider IDs per platform
- Replace `tmdbProviderId` (single int) with a many-to-many `platformTmdbProviders` join table so one platform entry can map to multiple TMDB provider IDs (e.g. a single "Max" platform covers several regional TMDB IDs)
- Add `isSubscription` flag to platforms to distinguish subscription services from transactional ones, replacing the old `displayOrder` field in API responses
- Add `getPlatformTmdbIds` / `getPlatformTmdbIdMap` helpers in `@sofa/core/platforms` and thread them through discover, explore, and platform list procedures
- Replace `providerId: number` with `platformId: string` (UUID) in discover input schema so the client never needs to resolve TMDB IDs
- Add `scripts/sync-tmdb-providers.ts` script to pull live provider data from TMDB and update `platforms.json`
- Split native title detail availability into separate "Stream" and "Buy or Rent" sections matching the web pattern
- Add two new DB migrations for the join table and platform schema changes
2026-03-24 20:54:34 -04:00

96 lines
3.0 KiB
TypeScript

/**
* Pulls all watch providers from the TMDB API, updates logo paths for existing
* providers, and auto-appends any missing ones to platforms.json.
*
* Usage: bun scripts/sync-tmdb-providers.ts [--region US]
*/
import type { SeedPlatform } from "../packages/db/src/seed-platforms";
import { getWatchProviderList } from "../packages/tmdb/src/client";
const region = process.argv.includes("--region")
? (process.argv[process.argv.indexOf("--region") + 1] ?? "US")
: "US";
console.log(`Fetching TMDB watch providers for region: ${region}\n`);
const [movieProviders, tvProviders] = await Promise.all([
getWatchProviderList("movie", region),
getWatchProviderList("tv", region),
]);
// Union by provider_id, preferring movie entry for metadata
const allProviders = new Map<
number,
{
provider_id: number;
provider_name: string;
logo_path: string | null;
display_priorities: Record<string, number>;
}
>();
for (const p of [...movieProviders, ...tvProviders]) {
if (!allProviders.has(p.provider_id)) {
allProviders.set(p.provider_id, p);
}
}
// Read current platforms JSON
const jsonPath = new URL("../packages/db/src/platforms.json", import.meta.url).pathname;
const seedData: SeedPlatform[] = await Bun.file(jsonPath).json();
// Build set of known TMDB IDs
const knownIds = new Set(seedData.flatMap((p) => p.tmdbProviderIds));
// Update logo paths for existing entries
let logosUpdated = 0;
for (const entry of seedData) {
const firstId = entry.tmdbProviderIds[0];
if (firstId == null) continue;
const tmdbProvider = allProviders.get(firstId);
if (!tmdbProvider?.logo_path) continue;
if (entry.logoPath !== tmdbProvider.logo_path) {
entry.logoPath = tmdbProvider.logo_path;
logosUpdated++;
}
}
console.log(`Updated ${logosUpdated} logo paths for existing entries\n`);
// Find missing providers
const missing = [...allProviders.values()]
.filter((p) => !knownIds.has(p.provider_id))
.sort((a, b) => {
const aPriority = a.display_priorities?.[region] ?? 999;
const bPriority = b.display_priorities?.[region] ?? 999;
return aPriority - bPriority;
});
console.log(`Total TMDB providers for ${region}: ${allProviders.size}`);
console.log(`Already in seed data: ${knownIds.size}`);
console.log(`Missing: ${missing.length}\n`);
if (missing.length > 0) {
for (const p of missing) {
seedData.push({
tmdbProviderIds: [p.provider_id],
name: p.provider_name,
logoPath: p.logo_path,
urlTemplate: "",
isSubscription: true,
});
}
console.log(`Added ${missing.length} new providers`);
console.log("\nTop 20 added:");
for (const p of missing.slice(0, 20)) {
const priority = p.display_priorities?.[region] ?? "?";
console.log(` [${p.provider_id}] ${p.provider_name} (priority: ${priority})`);
}
if (missing.length > 20) {
console.log(` ... and ${missing.length - 20} more`);
}
}
await Bun.write(jsonPath, JSON.stringify(seedData, null, 2) + "\n");
console.log(`\nWrote ${seedData.length} providers to platforms.json`);