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
This commit is contained in:
jake committed 2026-03-24 20:54:34 -04:00
1 parent 7fedccb4fd
commit b0172a2b25
58 files changed
+8291 -907

No files matched your search

+68 -22
View File
@@ -471,30 +471,76 @@ export default function TitleDetailScreen() {
iconColor={titleAccent}
/>
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={titleAvailabilityContentStyle}
>
{availability.map((offer) => (
<View key={`${offer.platformId}-${offer.offerType}`} className="items-center">
{offer.logoPath && (
<Image
source={{ uri: offer.logoPath }}
style={titleDetailStyles.providerLogo}
contentFit="cover"
/>
{(() => {
const stream = availability.filter((o) => o.offerType === "stream");
const purchase = availability.filter((o) => o.offerType === "purchase");
return (
<View className="gap-3">
{stream.length > 0 && (
<View>
<Text className="text-muted-foreground/60 mb-1.5 px-4 text-[10px] font-medium tracking-wider uppercase">
{t`Stream`}
</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={titleAvailabilityContentStyle}
>
{stream.map((offer) => (
<View key={offer.platformId} className="items-center">
{offer.logoPath && (
<Image
source={{ uri: offer.logoPath }}
style={titleDetailStyles.providerLogo}
contentFit="cover"
/>
)}
<Text
maxFontSizeMultiplier={1.0}
className="text-muted-foreground mt-1 max-w-[60px] text-center text-xs"
numberOfLines={1}
>
{offer.providerName}
</Text>
</View>
))}
</ScrollView>
</View>
)}
{purchase.length > 0 && (
<View>
<Text className="text-muted-foreground/60 mb-1.5 px-4 text-[10px] font-medium tracking-wider uppercase">
{t`Buy or Rent`}
</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={titleAvailabilityContentStyle}
>
{purchase.map((offer) => (
<View key={offer.platformId} className="items-center">
{offer.logoPath && (
<Image
source={{ uri: offer.logoPath }}
style={titleDetailStyles.providerLogo}
contentFit="cover"
/>
)}
<Text
maxFontSizeMultiplier={1.0}
className="text-muted-foreground mt-1 max-w-[60px] text-center text-xs"
numberOfLines={1}
>
{offer.providerName}
</Text>
</View>
))}
</ScrollView>
</View>
)}
<Text
maxFontSizeMultiplier={1.0}
className="text-muted-foreground mt-1 max-w-[60px] text-center text-xs"
numberOfLines={1}
>
{offer.providerName}
</Text>
</View>
))}
</ScrollView>
);
})()}
</Animated.View>
)}