mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-12 13:00:53 +00:00
chore: improved tmdb enrichment logic
This commit is contained in:
parent
3d5a9ebf42
commit
4aa22cc1c3
8 changed files with 413 additions and 343 deletions
|
|
@ -283,7 +283,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
if (!watchedSet || !showId) return false;
|
if (!watchedSet || !showId) return false;
|
||||||
const cleanShowId = showId.startsWith('tt') ? showId : `tt${showId}`;
|
const cleanShowId = showId.startsWith('tt') ? showId : `tt${showId}`;
|
||||||
return watchedSet.has(`${cleanShowId}:${season}:${episode}`) ||
|
return watchedSet.has(`${cleanShowId}:${season}:${episode}`) ||
|
||||||
watchedSet.has(`${showId}:${season}:${episode}`);
|
watchedSet.has(`${showId}:${season}:${episode}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const video of sortedVideos) {
|
for (const video of sortedVideos) {
|
||||||
|
|
@ -379,7 +379,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
const progressPercent =
|
const progressPercent =
|
||||||
progress.duration > 0
|
progress.duration > 0
|
||||||
? (progress.currentTime / progress.duration) * 100
|
? (progress.currentTime / progress.duration) * 100
|
||||||
: 0;
|
: 0;
|
||||||
// Skip fully watched movies
|
// Skip fully watched movies
|
||||||
if (type === 'movie' && progressPercent >= 85) continue;
|
if (type === 'movie' && progressPercent >= 85) continue;
|
||||||
// Skip movies with no actual progress (ensure > 0%)
|
// Skip movies with no actual progress (ensure > 0%)
|
||||||
|
|
@ -711,7 +711,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
const movieKey = `movie:${imdbId}`;
|
const movieKey = `movie:${imdbId}`;
|
||||||
if (recentlyRemovedRef.current.has(movieKey)) continue;
|
if (recentlyRemovedRef.current.has(movieKey)) continue;
|
||||||
|
|
||||||
const cachedData = await getCachedMetadata('movie', imdbId, item.addonId);
|
const cachedData = await getCachedMetadata('movie', imdbId);
|
||||||
if (!cachedData?.basicContent) continue;
|
if (!cachedData?.basicContent) continue;
|
||||||
|
|
||||||
const pausedAt = new Date(item.paused_at).getTime();
|
const pausedAt = new Date(item.paused_at).getTime();
|
||||||
|
|
@ -743,7 +743,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedData = await getCachedMetadata('series', showImdb, item.addonId);
|
const cachedData = await getCachedMetadata('series', showImdb);
|
||||||
if (!cachedData?.basicContent) continue;
|
if (!cachedData?.basicContent) continue;
|
||||||
|
|
||||||
traktBatch.push({
|
traktBatch.push({
|
||||||
|
|
@ -1204,41 +1204,40 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
padding: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
|
padding: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const isUpNext = item.type === 'series' && item.progress === 0;
|
const isUpNext = item.type === 'series' && item.progress === 0;
|
||||||
return (
|
return (
|
||||||
<View style={styles.titleRow}>
|
<View style={styles.titleRow}>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.contentTitle,
|
styles.contentTitle,
|
||||||
{
|
{
|
||||||
color: currentTheme.colors.highEmphasis,
|
color: currentTheme.colors.highEmphasis,
|
||||||
fontSize: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 17 : 16
|
fontSize: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 17 : 16
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
{isUpNext && (
|
{isUpNext && (
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.progressBadge,
|
styles.progressBadge,
|
||||||
{
|
{
|
||||||
backgroundColor: currentTheme.colors.primary,
|
backgroundColor: currentTheme.colors.primary,
|
||||||
paddingHorizontal: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
|
paddingHorizontal: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
|
||||||
paddingVertical: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3
|
paddingVertical: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.progressText,
|
styles.progressText,
|
||||||
{ fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12 }
|
{ fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12 }
|
||||||
]}>Up Next</Text>
|
]}>Up Next</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Episode Info or Year */}
|
{/* Episode Info or Year */}
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ import {
|
||||||
TraktWatchlistItem,
|
TraktWatchlistItem,
|
||||||
TraktCollectionItem,
|
TraktCollectionItem,
|
||||||
TraktRatingItem,
|
TraktRatingItem,
|
||||||
TraktPlaybackItem
|
TraktPlaybackItem,
|
||||||
|
traktService
|
||||||
} from '../services/traktService';
|
} from '../services/traktService';
|
||||||
|
|
||||||
interface TraktContextProps {
|
interface TraktContextProps {
|
||||||
|
|
@ -37,6 +38,9 @@ interface TraktContextProps {
|
||||||
removeFromCollection: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
|
removeFromCollection: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
|
||||||
isInWatchlist: (imdbId: string, type: 'movie' | 'show') => boolean;
|
isInWatchlist: (imdbId: string, type: 'movie' | 'show') => boolean;
|
||||||
isInCollection: (imdbId: string, type: 'movie' | 'show') => boolean;
|
isInCollection: (imdbId: string, type: 'movie' | 'show') => boolean;
|
||||||
|
// Maintenance mode
|
||||||
|
isMaintenanceMode: boolean;
|
||||||
|
maintenanceMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TraktContext = createContext<TraktContextProps | undefined>(undefined);
|
const TraktContext = createContext<TraktContextProps | undefined>(undefined);
|
||||||
|
|
@ -44,8 +48,15 @@ const TraktContext = createContext<TraktContextProps | undefined>(undefined);
|
||||||
export function TraktProvider({ children }: { children: ReactNode }) {
|
export function TraktProvider({ children }: { children: ReactNode }) {
|
||||||
const traktIntegration = useTraktIntegration();
|
const traktIntegration = useTraktIntegration();
|
||||||
|
|
||||||
|
// Add maintenance mode values to the context
|
||||||
|
const contextValue: TraktContextProps = {
|
||||||
|
...traktIntegration,
|
||||||
|
isMaintenanceMode: traktService.isMaintenanceMode(),
|
||||||
|
maintenanceMessage: traktService.getMaintenanceMessage(),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TraktContext.Provider value={traktIntegration}>
|
<TraktContext.Provider value={contextValue}>
|
||||||
{children}
|
{children}
|
||||||
</TraktContext.Provider>
|
</TraktContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -550,7 +550,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
if (__DEV__) logger.log('Fetching movie details from TMDB for:', tmdbId);
|
if (__DEV__) logger.log('Fetching movie details from TMDB for:', tmdbId);
|
||||||
const movieDetails = await tmdbService.getMovieDetails(
|
const movieDetails = await tmdbService.getMovieDetails(
|
||||||
tmdbId,
|
tmdbId,
|
||||||
settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}-US` : 'en-US'
|
settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en'
|
||||||
);
|
);
|
||||||
if (movieDetails) {
|
if (movieDetails) {
|
||||||
const imdbId = movieDetails.imdb_id || movieDetails.external_ids?.imdb_id;
|
const imdbId = movieDetails.imdb_id || movieDetails.external_ids?.imdb_id;
|
||||||
|
|
@ -634,7 +634,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
try {
|
try {
|
||||||
const showDetails = await tmdbService.getTVShowDetails(
|
const showDetails = await tmdbService.getTVShowDetails(
|
||||||
parseInt(tmdbId),
|
parseInt(tmdbId),
|
||||||
settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}-US` : 'en-US'
|
settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en'
|
||||||
);
|
);
|
||||||
if (showDetails) {
|
if (showDetails) {
|
||||||
// OPTIMIZATION: Fetch external IDs, credits, and logo in parallel
|
// OPTIMIZATION: Fetch external IDs, credits, and logo in parallel
|
||||||
|
|
@ -824,9 +824,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
// Store addon logo before TMDB enrichment overwrites it
|
// Store addon logo before TMDB enrichment overwrites it
|
||||||
const addonLogo = (finalMetadata as any).logo;
|
const addonLogo = (finalMetadata as any).logo;
|
||||||
|
|
||||||
// If localization is enabled, merge TMDB localized text (name/overview) before first render
|
// If localization is enabled AND title/description enrichment is enabled, merge TMDB localized text (name/overview) before first render
|
||||||
try {
|
try {
|
||||||
if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) {
|
if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata && settings.tmdbEnrichTitleDescription) {
|
||||||
const tmdbSvc = TMDBService.getInstance();
|
const tmdbSvc = TMDBService.getInstance();
|
||||||
let finalTmdbId: number | null = tmdbId;
|
let finalTmdbId: number | null = tmdbId;
|
||||||
if (!finalTmdbId) {
|
if (!finalTmdbId) {
|
||||||
|
|
@ -857,8 +857,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
|
|
||||||
finalMetadata = {
|
finalMetadata = {
|
||||||
...finalMetadata,
|
...finalMetadata,
|
||||||
name: finalMetadata.name || localized.title,
|
name: localized.title || finalMetadata.name,
|
||||||
description: finalMetadata.description || localized.overview,
|
description: localized.overview || finalMetadata.description,
|
||||||
movieDetails: movieDetailsObj,
|
movieDetails: movieDetailsObj,
|
||||||
...(productionInfo.length > 0 && { networks: productionInfo }),
|
...(productionInfo.length > 0 && { networks: productionInfo }),
|
||||||
};
|
};
|
||||||
|
|
@ -894,8 +894,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
|
|
||||||
finalMetadata = {
|
finalMetadata = {
|
||||||
...finalMetadata,
|
...finalMetadata,
|
||||||
name: finalMetadata.name || localized.name,
|
name: localized.name || finalMetadata.name,
|
||||||
description: finalMetadata.description || localized.overview,
|
description: localized.overview || finalMetadata.description,
|
||||||
tvDetails,
|
tvDetails,
|
||||||
...(productionInfo.length > 0 && { networks: productionInfo }),
|
...(productionInfo.length > 0 && { networks: productionInfo }),
|
||||||
};
|
};
|
||||||
|
|
@ -909,14 +909,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
|
|
||||||
// Centralized logo fetching logic
|
// Centralized logo fetching logic
|
||||||
try {
|
try {
|
||||||
if (addonLogo) {
|
// When TMDB enrichment AND logos are enabled, prioritize TMDB logo over addon logo
|
||||||
finalMetadata.logo = addonLogo;
|
if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichLogos) {
|
||||||
if (__DEV__) {
|
|
||||||
console.log('[useMetadata] Using addon-provided logo:', { hasLogo: true });
|
|
||||||
}
|
|
||||||
// Check both master switch AND granular logos setting
|
|
||||||
} else if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichLogos) {
|
|
||||||
// Only use TMDB logos when both enrichment AND logos option are ON
|
|
||||||
const tmdbService = TMDBService.getInstance();
|
const tmdbService = TMDBService.getInstance();
|
||||||
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
||||||
const contentType = type === 'series' ? 'tv' : 'movie';
|
const contentType = type === 'series' ? 'tv' : 'movie';
|
||||||
|
|
@ -932,23 +926,26 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
|
|
||||||
if (tmdbIdForLogo) {
|
if (tmdbIdForLogo) {
|
||||||
const logoUrl = await tmdbService.getContentLogo(contentType, tmdbIdForLogo, preferredLanguage);
|
const logoUrl = await tmdbService.getContentLogo(contentType, tmdbIdForLogo, preferredLanguage);
|
||||||
finalMetadata.logo = logoUrl || undefined; // TMDB logo or undefined (no addon fallback)
|
// Use TMDB logo if found, otherwise fall back to addon logo
|
||||||
|
finalMetadata.logo = logoUrl || addonLogo || undefined;
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('[useMetadata] Logo fetch result:', {
|
console.log('[useMetadata] Logo fetch result:', {
|
||||||
contentType,
|
contentType,
|
||||||
tmdbIdForLogo,
|
tmdbIdForLogo,
|
||||||
preferredLanguage,
|
preferredLanguage,
|
||||||
logoUrl: !!logoUrl,
|
tmdbLogoFound: !!logoUrl,
|
||||||
|
usingAddonFallback: !logoUrl && !!addonLogo,
|
||||||
enrichmentEnabled: true
|
enrichmentEnabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
finalMetadata.logo = undefined; // No TMDB ID means no logo
|
// No TMDB ID, fall back to addon logo
|
||||||
if (__DEV__) console.log('[useMetadata] No TMDB ID found for logo, will show text title');
|
finalMetadata.logo = addonLogo || undefined;
|
||||||
|
if (__DEV__) console.log('[useMetadata] No TMDB ID found for logo, using addon logo');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// When enrichment or logos is OFF, keep addon logo or undefined
|
// When enrichment or logos is OFF, use addon logo
|
||||||
finalMetadata.logo = finalMetadata.logo || undefined;
|
finalMetadata.logo = addonLogo || finalMetadata.logo || undefined;
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('[useMetadata] TMDB logo enrichment disabled, using addon logo:', {
|
console.log('[useMetadata] TMDB logo enrichment disabled, using addon logo:', {
|
||||||
hasAddonLogo: !!finalMetadata.logo,
|
hasAddonLogo: !!finalMetadata.logo,
|
||||||
|
|
@ -1125,10 +1122,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
// Fetch season posters from TMDB only if enrichment AND season posters are enabled
|
// Fetch season posters from TMDB only if enrichment AND season posters are enabled
|
||||||
if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichSeasonPosters) {
|
if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichSeasonPosters) {
|
||||||
try {
|
try {
|
||||||
|
const lang = settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}` : 'en';
|
||||||
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
|
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
|
||||||
if (tmdbIdToUse) {
|
if (tmdbIdToUse) {
|
||||||
if (!tmdbId) setTmdbId(tmdbIdToUse);
|
if (!tmdbId) setTmdbId(tmdbIdToUse);
|
||||||
const showDetails = await tmdbService.getTVShowDetails(tmdbIdToUse);
|
const showDetails = await tmdbService.getTVShowDetails(tmdbIdToUse, lang);
|
||||||
if (showDetails?.seasons) {
|
if (showDetails?.seasons) {
|
||||||
Object.keys(groupedAddonEpisodes).forEach(seasonStr => {
|
Object.keys(groupedAddonEpisodes).forEach(seasonStr => {
|
||||||
const seasonNum = parseInt(seasonStr, 10);
|
const seasonNum = parseInt(seasonStr, 10);
|
||||||
|
|
@ -1156,7 +1154,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
try {
|
try {
|
||||||
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
|
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
|
||||||
if (tmdbIdToUse) {
|
if (tmdbIdToUse) {
|
||||||
const lang = `${settings.tmdbLanguagePreference || 'en'}-US`;
|
// Use just the language code (e.g., 'ar', not 'ar-US') for TMDB API
|
||||||
|
const lang = settings.tmdbLanguagePreference || 'en';
|
||||||
const seasons = Object.keys(groupedAddonEpisodes).map(Number);
|
const seasons = Object.keys(groupedAddonEpisodes).map(Number);
|
||||||
for (const seasonNum of seasons) {
|
for (const seasonNum of seasons) {
|
||||||
const seasonEps = groupedAddonEpisodes[seasonNum];
|
const seasonEps = groupedAddonEpisodes[seasonNum];
|
||||||
|
|
@ -1264,13 +1263,14 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
|
|
||||||
// Fallback to TMDB if no addon episodes
|
// Fallback to TMDB if no addon episodes
|
||||||
logger.log('📺 No addon episodes found, falling back to TMDB');
|
logger.log('📺 No addon episodes found, falling back to TMDB');
|
||||||
|
const lang = settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}` : 'en';
|
||||||
const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id);
|
const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id);
|
||||||
if (tmdbIdResult) {
|
if (tmdbIdResult) {
|
||||||
setTmdbId(tmdbIdResult);
|
setTmdbId(tmdbIdResult);
|
||||||
|
|
||||||
const [allEpisodes, showDetails] = await Promise.all([
|
const [allEpisodes, showDetails] = await Promise.all([
|
||||||
tmdbService.getAllEpisodes(tmdbIdResult),
|
tmdbService.getAllEpisodes(tmdbIdResult, lang),
|
||||||
tmdbService.getTVShowDetails(tmdbIdResult)
|
tmdbService.getTVShowDetails(tmdbIdResult, lang)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const transformedEpisodes: GroupedEpisodes = {};
|
const transformedEpisodes: GroupedEpisodes = {};
|
||||||
|
|
@ -2038,7 +2038,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
setLoadingRecommendations(true);
|
setLoadingRecommendations(true);
|
||||||
try {
|
try {
|
||||||
const tmdbService = TMDBService.getInstance();
|
const tmdbService = TMDBService.getInstance();
|
||||||
const results = await tmdbService.getRecommendations(type === 'movie' ? 'movie' : 'tv', String(tmdbId));
|
const lang = settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en';
|
||||||
|
const results = await tmdbService.getRecommendations(type === 'movie' ? 'movie' : 'tv', String(tmdbId), lang);
|
||||||
|
|
||||||
// Convert TMDB results to StreamingContent format (simplified)
|
// Convert TMDB results to StreamingContent format (simplified)
|
||||||
const formattedRecommendations: StreamingContent[] = results.map((item: any) => ({
|
const formattedRecommendations: StreamingContent[] = results.map((item: any) => ({
|
||||||
|
|
@ -2056,7 +2057,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRecommendations(false);
|
setLoadingRecommendations(false);
|
||||||
}
|
}
|
||||||
}, [tmdbId, type]);
|
}, [tmdbId, type, settings.useTmdbLocalizedMetadata, settings.tmdbLanguagePreference]);
|
||||||
|
|
||||||
// Fetch TMDB ID if needed and then recommendations
|
// Fetch TMDB ID if needed and then recommendations
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ export interface AppSettings {
|
||||||
tmdbEnrichMovieDetails: boolean; // Show movie details (budget, revenue, tagline, etc.)
|
tmdbEnrichMovieDetails: boolean; // Show movie details (budget, revenue, tagline, etc.)
|
||||||
tmdbEnrichTvDetails: boolean; // Show TV details (status, seasons count, networks, etc.)
|
tmdbEnrichTvDetails: boolean; // Show TV details (status, seasons count, networks, etc.)
|
||||||
tmdbEnrichCollections: boolean; // Show movie collections/franchises
|
tmdbEnrichCollections: boolean; // Show movie collections/franchises
|
||||||
|
tmdbEnrichTitleDescription: boolean; // Use TMDB title/description (overrides addon when localization enabled)
|
||||||
// Trakt integration
|
// Trakt integration
|
||||||
showTraktComments: boolean; // Show Trakt comments in metadata screens
|
showTraktComments: boolean; // Show Trakt comments in metadata screens
|
||||||
// Continue Watching behavior
|
// Continue Watching behavior
|
||||||
|
|
@ -176,6 +177,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
tmdbEnrichMovieDetails: true,
|
tmdbEnrichMovieDetails: true,
|
||||||
tmdbEnrichTvDetails: true,
|
tmdbEnrichTvDetails: true,
|
||||||
tmdbEnrichCollections: true,
|
tmdbEnrichCollections: true,
|
||||||
|
tmdbEnrichTitleDescription: true, // Enabled by default for backward compatibility
|
||||||
// Trakt integration
|
// Trakt integration
|
||||||
showTraktComments: true, // Show Trakt comments by default when authenticated
|
showTraktComments: true, // Show Trakt comments by default when authenticated
|
||||||
// Continue Watching behavior
|
// Continue Watching behavior
|
||||||
|
|
|
||||||
|
|
@ -677,6 +677,23 @@ const TMDBSettingsScreen = () => {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Title & Description */}
|
||||||
|
<View style={styles.settingRow}>
|
||||||
|
<View style={styles.settingTextContainer}>
|
||||||
|
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Title & Description</Text>
|
||||||
|
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
|
Use TMDb localized title and overview text
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={settings.tmdbEnrichTitleDescription}
|
||||||
|
onValueChange={(v) => updateSetting('tmdbEnrichTitleDescription', v)}
|
||||||
|
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
|
||||||
|
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
|
||||||
|
ios_backgroundColor={'rgba(255,255,255,0.1)'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Title Logos */}
|
{/* Title Logos */}
|
||||||
<View style={styles.settingRow}>
|
<View style={styles.settingRow}>
|
||||||
<View style={styles.settingTextContainer}>
|
<View style={styles.settingTextContainer}>
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
'Sign Out',
|
'Sign Out',
|
||||||
'Are you sure you want to sign out of your Trakt account?',
|
'Are you sure you want to sign out of your Trakt account?',
|
||||||
[
|
[
|
||||||
{ label: 'Cancel', onPress: () => {} },
|
{ label: 'Cancel', onPress: () => { } },
|
||||||
{
|
{
|
||||||
label: 'Sign Out',
|
label: 'Sign Out',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
|
|
@ -243,6 +243,19 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
Trakt Settings
|
Trakt Settings
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* Maintenance Mode Banner */}
|
||||||
|
{traktService.isMaintenanceMode() && (
|
||||||
|
<View style={styles.maintenanceBanner}>
|
||||||
|
<MaterialIcons name="engineering" size={24} color="#FFF" />
|
||||||
|
<View style={styles.maintenanceBannerTextContainer}>
|
||||||
|
<Text style={styles.maintenanceBannerTitle}>Under Maintenance</Text>
|
||||||
|
<Text style={styles.maintenanceBannerMessage}>
|
||||||
|
{traktService.getMaintenanceMessage()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
|
@ -255,6 +268,38 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||||
</View>
|
</View>
|
||||||
|
) : traktService.isMaintenanceMode() ? (
|
||||||
|
<View style={styles.signInContainer}>
|
||||||
|
<TraktIcon
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
style={[styles.traktLogo, { opacity: 0.5 }]}
|
||||||
|
/>
|
||||||
|
<Text style={[
|
||||||
|
styles.signInTitle,
|
||||||
|
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||||
|
]}>
|
||||||
|
Trakt Unavailable
|
||||||
|
</Text>
|
||||||
|
<Text style={[
|
||||||
|
styles.signInDescription,
|
||||||
|
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||||
|
]}>
|
||||||
|
The Trakt integration is temporarily paused for maintenance. All syncing and authentication is disabled until maintenance is complete.
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
{ backgroundColor: currentTheme.colors.border, opacity: 0.6 }
|
||||||
|
]}
|
||||||
|
disabled={true}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="engineering" size={20} color={currentTheme.colors.mediumEmphasis} style={{ marginRight: 8 }} />
|
||||||
|
<Text style={[styles.buttonText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
|
Service Under Maintenance
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
) : isAuthenticated && userProfile ? (
|
) : isAuthenticated && userProfile ? (
|
||||||
<View style={styles.profileContainer}>
|
<View style={styles.profileContainer}>
|
||||||
<View style={styles.profileHeader}>
|
<View style={styles.profileHeader}>
|
||||||
|
|
@ -704,6 +749,31 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
},
|
},
|
||||||
|
// Maintenance mode styles
|
||||||
|
maintenanceBanner: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#E67E22',
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginBottom: 16,
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
maintenanceBannerTextContainer: {
|
||||||
|
marginLeft: 12,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
maintenanceBannerTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#FFF',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
maintenanceBannerMessage: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: '#FFF',
|
||||||
|
opacity: 0.9,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default TraktSettingsScreen;
|
export default TraktSettingsScreen;
|
||||||
|
|
@ -7,12 +7,6 @@ const DEFAULT_API_KEY = 'd131017ccc6e5462a81c9304d21476de';
|
||||||
const BASE_URL = 'https://api.themoviedb.org/3';
|
const BASE_URL = 'https://api.themoviedb.org/3';
|
||||||
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
||||||
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
|
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
|
||||||
// Remote cache configuration
|
|
||||||
const REMOTE_CACHE_URL = process.env.EXPO_PUBLIC_CACHE_SERVER_URL;
|
|
||||||
const USE_REMOTE_CACHE = process.env.EXPO_PUBLIC_USE_REMOTE_CACHE === 'true';
|
|
||||||
const REMOTE_CACHE_NAMESPACE = 'tmdb';
|
|
||||||
// Allow temporarily disabling local MMKV cache (read/write)
|
|
||||||
const DISABLE_LOCAL_CACHE = process.env.EXPO_PUBLIC_DISABLE_LOCAL_CACHE === 'true';
|
|
||||||
|
|
||||||
// Cache configuration
|
// Cache configuration
|
||||||
const TMDB_CACHE_PREFIX = 'tmdb_cache_';
|
const TMDB_CACHE_PREFIX = 'tmdb_cache_';
|
||||||
|
|
@ -140,52 +134,6 @@ export class TMDBService {
|
||||||
this.loadApiKey();
|
this.loadApiKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote cache helpers
|
|
||||||
*/
|
|
||||||
private async remoteGetCachedData<T>(key: string): Promise<T | null> {
|
|
||||||
if (!USE_REMOTE_CACHE || !REMOTE_CACHE_URL) return null;
|
|
||||||
try {
|
|
||||||
const url = `${REMOTE_CACHE_URL}/cache/${REMOTE_CACHE_NAMESPACE}/${encodeURIComponent(key)}`;
|
|
||||||
const response = await axios.get(url, { headers: { 'Content-Type': 'application/json' } });
|
|
||||||
const payload = response.data;
|
|
||||||
if (payload && Object.prototype.hasOwnProperty.call(payload, 'data')) {
|
|
||||||
// Warm local cache for faster subsequent reads (skip if disabled)
|
|
||||||
if (!DISABLE_LOCAL_CACHE) {
|
|
||||||
this.setCachedData(key, payload.data);
|
|
||||||
}
|
|
||||||
logger.log(`[TMDB Remote Cache] ✅ HIT: ${key}`);
|
|
||||||
return payload.data as T;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (_) {
|
|
||||||
logger.log(`[TMDB Remote Cache] ❌ MISS: ${key}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async remoteSetCachedData(key: string, data: any): Promise<void> {
|
|
||||||
if (!USE_REMOTE_CACHE || !REMOTE_CACHE_URL) return;
|
|
||||||
try {
|
|
||||||
const url = `${REMOTE_CACHE_URL}/cache/${REMOTE_CACHE_NAMESPACE}/${encodeURIComponent(key)}`;
|
|
||||||
await axios.put(url, { data, ttlMs: CACHE_TTL_MS }, { headers: { 'Content-Type': 'application/json' } });
|
|
||||||
logger.log(`[TMDB Remote Cache] 💾 STORED: ${key}`);
|
|
||||||
} catch (_) {
|
|
||||||
// best-effort only
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async remoteClearAllCache(): Promise<void> {
|
|
||||||
if (!USE_REMOTE_CACHE || !REMOTE_CACHE_URL) return;
|
|
||||||
try {
|
|
||||||
const url = `${REMOTE_CACHE_URL}/cache/${REMOTE_CACHE_NAMESPACE}/clear`;
|
|
||||||
await axios.post(url, {}, { headers: { 'Content-Type': 'application/json' } });
|
|
||||||
logger.log(`[TMDB Remote Cache] 🗑️ CLEARED namespace ${REMOTE_CACHE_NAMESPACE}`);
|
|
||||||
} catch (_) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a unique cache key from endpoint and parameters
|
* Generate a unique cache key from endpoint and parameters
|
||||||
*/
|
*/
|
||||||
|
|
@ -206,10 +154,6 @@ export class TMDBService {
|
||||||
* Retrieve cached data if not expired
|
* Retrieve cached data if not expired
|
||||||
*/
|
*/
|
||||||
private getCachedData<T>(key: string): T | null {
|
private getCachedData<T>(key: string): T | null {
|
||||||
if (DISABLE_LOCAL_CACHE) {
|
|
||||||
logger.log(`[TMDB Cache] 🚫 LOCAL DISABLED: ${key}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const cachedStr = mmkvStorage.getString(key);
|
const cachedStr = mmkvStorage.getString(key);
|
||||||
if (!cachedStr) {
|
if (!cachedStr) {
|
||||||
|
|
@ -236,17 +180,11 @@ export class TMDBService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get from local cache
|
||||||
|
*/
|
||||||
private async getFromCacheOrRemote<T>(key: string): Promise<T | null> {
|
private async getFromCacheOrRemote<T>(key: string): Promise<T | null> {
|
||||||
// Local-first: serve from MMKV if present; else try remote and warm local
|
return this.getCachedData<T>(key);
|
||||||
if (!DISABLE_LOCAL_CACHE) {
|
|
||||||
const local = this.getCachedData<T>(key);
|
|
||||||
if (local !== null) return local;
|
|
||||||
}
|
|
||||||
if (USE_REMOTE_CACHE && REMOTE_CACHE_URL) {
|
|
||||||
const remote = await this.remoteGetCachedData<T>(key);
|
|
||||||
if (remote !== null) return remote;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -262,19 +200,12 @@ export class TMDBService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!DISABLE_LOCAL_CACHE) {
|
|
||||||
const cacheEntry = {
|
const cacheEntry = {
|
||||||
data,
|
data,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
mmkvStorage.setString(key, JSON.stringify(cacheEntry));
|
mmkvStorage.setString(key, JSON.stringify(cacheEntry));
|
||||||
logger.log(`[TMDB Cache] 💾 STORED: ${key}`);
|
logger.log(`[TMDB Cache] 💾 STORED: ${key}`);
|
||||||
} else {
|
|
||||||
logger.log(`[TMDB Cache] ⛔ LOCAL WRITE SKIPPED: ${key}`);
|
|
||||||
}
|
|
||||||
// Best-effort remote write
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.remoteSetCachedData(key, data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore cache errors
|
// Ignore cache errors
|
||||||
}
|
}
|
||||||
|
|
@ -358,8 +289,8 @@ export class TMDBService {
|
||||||
/**
|
/**
|
||||||
* Search for a TV show by name
|
* Search for a TV show by name
|
||||||
*/
|
*/
|
||||||
async searchTVShow(query: string): Promise<TMDBShow[]> {
|
async searchTVShow(query: string, language: string = 'en-US'): Promise<TMDBShow[]> {
|
||||||
const cacheKey = this.generateCacheKey('search_tv', { query });
|
const cacheKey = this.generateCacheKey('search_tv', { query, language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<TMDBShow[]>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<TMDBShow[]>(cacheKey);
|
||||||
|
|
@ -372,7 +303,7 @@ export class TMDBService {
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
query,
|
query,
|
||||||
include_adult: false,
|
include_adult: false,
|
||||||
language: 'en-US',
|
language,
|
||||||
page: 1,
|
page: 1,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -601,8 +532,8 @@ export class TMDBService {
|
||||||
/**
|
/**
|
||||||
* Find TMDB ID by IMDB ID
|
* Find TMDB ID by IMDB ID
|
||||||
*/
|
*/
|
||||||
async findTMDBIdByIMDB(imdbId: string): Promise<number | null> {
|
async findTMDBIdByIMDB(imdbId: string, language: string = 'en-US'): Promise<number | null> {
|
||||||
const cacheKey = this.generateCacheKey('find_imdb', { imdbId });
|
const cacheKey = this.generateCacheKey('find_imdb', { imdbId, language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<number>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<number>(cacheKey);
|
||||||
|
|
@ -616,7 +547,7 @@ export class TMDBService {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
external_source: 'imdb_id',
|
external_source: 'imdb_id',
|
||||||
language: 'en-US',
|
language,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -658,11 +589,12 @@ export class TMDBService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all episodes for a TV show
|
* Get all episodes for a TV show
|
||||||
|
* @param language Language for localized episode names/overviews
|
||||||
*/
|
*/
|
||||||
async getAllEpisodes(tmdbId: number): Promise<{ [seasonNumber: number]: TMDBEpisode[] }> {
|
async getAllEpisodes(tmdbId: number, language: string = 'en-US'): Promise<{ [seasonNumber: number]: TMDBEpisode[] }> {
|
||||||
try {
|
try {
|
||||||
// First get the show details to know how many seasons there are
|
// First get the show details to know how many seasons there are
|
||||||
const showDetails = await this.getTVShowDetails(tmdbId);
|
const showDetails = await this.getTVShowDetails(tmdbId, language);
|
||||||
if (!showDetails) return {};
|
if (!showDetails) return {};
|
||||||
|
|
||||||
const allEpisodes: { [seasonNumber: number]: TMDBEpisode[] } = {};
|
const allEpisodes: { [seasonNumber: number]: TMDBEpisode[] } = {};
|
||||||
|
|
@ -671,7 +603,7 @@ export class TMDBService {
|
||||||
const seasonPromises = showDetails.seasons
|
const seasonPromises = showDetails.seasons
|
||||||
.filter(season => season.season_number > 0) // Filter out specials (season 0)
|
.filter(season => season.season_number > 0) // Filter out specials (season 0)
|
||||||
.map(async season => {
|
.map(async season => {
|
||||||
const seasonDetails = await this.getSeasonDetails(tmdbId, season.season_number);
|
const seasonDetails = await this.getSeasonDetails(tmdbId, season.season_number, showDetails.name, language);
|
||||||
if (seasonDetails && seasonDetails.episodes) {
|
if (seasonDetails && seasonDetails.episodes) {
|
||||||
allEpisodes[season.season_number] = seasonDetails.episodes;
|
allEpisodes[season.season_number] = seasonDetails.episodes;
|
||||||
}
|
}
|
||||||
|
|
@ -727,8 +659,8 @@ export class TMDBService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCredits(tmdbId: number, type: string) {
|
async getCredits(tmdbId: number, type: string, language: string = 'en-US') {
|
||||||
const cacheKey = this.generateCacheKey(`${type}_${tmdbId}_credits`);
|
const cacheKey = this.generateCacheKey(`${type}_${tmdbId}_credits`, { language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<{ cast: any[]; crew: any[] }>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<{ cast: any[]; crew: any[] }>(cacheKey);
|
||||||
|
|
@ -738,7 +670,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/${type === 'series' ? 'tv' : 'movie'}/${tmdbId}/credits`, {
|
const response = await axios.get(`${BASE_URL}/${type === 'series' ? 'tv' : 'movie'}/${tmdbId}/credits`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = {
|
const data = {
|
||||||
|
|
@ -752,8 +684,8 @@ export class TMDBService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPersonDetails(personId: number) {
|
async getPersonDetails(personId: number, language: string = 'en-US') {
|
||||||
const cacheKey = this.generateCacheKey(`person_${personId}`);
|
const cacheKey = this.generateCacheKey(`person_${personId}`, { language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<any>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<any>(cacheKey);
|
||||||
|
|
@ -763,7 +695,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/person/${personId}`, {
|
const response = await axios.get(`${BASE_URL}/person/${personId}`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
@ -777,8 +709,8 @@ export class TMDBService {
|
||||||
/**
|
/**
|
||||||
* Get person's movie credits (cast and crew)
|
* Get person's movie credits (cast and crew)
|
||||||
*/
|
*/
|
||||||
async getPersonMovieCredits(personId: number) {
|
async getPersonMovieCredits(personId: number, language: string = 'en-US') {
|
||||||
const cacheKey = this.generateCacheKey(`person_${personId}_movie_credits`);
|
const cacheKey = this.generateCacheKey(`person_${personId}_movie_credits`, { language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<any>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<any>(cacheKey);
|
||||||
|
|
@ -788,7 +720,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/person/${personId}/movie_credits`, {
|
const response = await axios.get(`${BASE_URL}/person/${personId}/movie_credits`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
@ -802,8 +734,8 @@ export class TMDBService {
|
||||||
/**
|
/**
|
||||||
* Get person's TV credits (cast and crew)
|
* Get person's TV credits (cast and crew)
|
||||||
*/
|
*/
|
||||||
async getPersonTvCredits(personId: number) {
|
async getPersonTvCredits(personId: number, language: string = 'en-US') {
|
||||||
const cacheKey = this.generateCacheKey(`person_${personId}_tv_credits`);
|
const cacheKey = this.generateCacheKey(`person_${personId}_tv_credits`, { language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<any>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<any>(cacheKey);
|
||||||
|
|
@ -813,7 +745,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/person/${personId}/tv_credits`, {
|
const response = await axios.get(`${BASE_URL}/person/${personId}/tv_credits`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
@ -827,8 +759,8 @@ export class TMDBService {
|
||||||
/**
|
/**
|
||||||
* Get person's combined credits (movies and TV)
|
* Get person's combined credits (movies and TV)
|
||||||
*/
|
*/
|
||||||
async getPersonCombinedCredits(personId: number) {
|
async getPersonCombinedCredits(personId: number, language: string = 'en-US') {
|
||||||
const cacheKey = this.generateCacheKey(`person_${personId}_combined_credits`);
|
const cacheKey = this.generateCacheKey(`person_${personId}_combined_credits`, { language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<any>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<any>(cacheKey);
|
||||||
|
|
@ -838,7 +770,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/person/${personId}/combined_credits`, {
|
const response = await axios.get(`${BASE_URL}/person/${personId}/combined_credits`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
@ -899,8 +831,8 @@ export class TMDBService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchMulti(query: string): Promise<any[]> {
|
async searchMulti(query: string, language: string = 'en-US'): Promise<any[]> {
|
||||||
const cacheKey = this.generateCacheKey('search_multi', { query });
|
const cacheKey = this.generateCacheKey('search_multi', { query, language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<any[]>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<any[]>(cacheKey);
|
||||||
|
|
@ -912,7 +844,7 @@ export class TMDBService {
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
query,
|
query,
|
||||||
include_adult: false,
|
include_adult: false,
|
||||||
language: 'en-US',
|
language,
|
||||||
page: 1,
|
page: 1,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -1402,9 +1334,10 @@ export class TMDBService {
|
||||||
* Get trending movies or TV shows
|
* Get trending movies or TV shows
|
||||||
* @param type 'movie' or 'tv'
|
* @param type 'movie' or 'tv'
|
||||||
* @param timeWindow 'day' or 'week'
|
* @param timeWindow 'day' or 'week'
|
||||||
|
* @param language Language for localized results
|
||||||
*/
|
*/
|
||||||
async getTrending(type: 'movie' | 'tv', timeWindow: 'day' | 'week'): Promise<TMDBTrendingResult[]> {
|
async getTrending(type: 'movie' | 'tv', timeWindow: 'day' | 'week', language: string = 'en-US'): Promise<TMDBTrendingResult[]> {
|
||||||
const cacheKey = this.generateCacheKey(`trending_${type}_${timeWindow}`);
|
const cacheKey = this.generateCacheKey(`trending_${type}_${timeWindow}`, { language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
|
||||||
|
|
@ -1414,7 +1347,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/trending/${type}/${timeWindow}`, {
|
const response = await axios.get(`${BASE_URL}/trending/${type}/${timeWindow}`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1451,9 +1384,10 @@ export class TMDBService {
|
||||||
* Get popular movies or TV shows
|
* Get popular movies or TV shows
|
||||||
* @param type 'movie' or 'tv'
|
* @param type 'movie' or 'tv'
|
||||||
* @param page Page number for pagination
|
* @param page Page number for pagination
|
||||||
|
* @param language Language for localized results
|
||||||
*/
|
*/
|
||||||
async getPopular(type: 'movie' | 'tv', page: number = 1): Promise<TMDBTrendingResult[]> {
|
async getPopular(type: 'movie' | 'tv', page: number = 1, language: string = 'en-US'): Promise<TMDBTrendingResult[]> {
|
||||||
const cacheKey = this.generateCacheKey(`popular_${type}`, { page });
|
const cacheKey = this.generateCacheKey(`popular_${type}`, { page, language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
|
||||||
|
|
@ -1463,7 +1397,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/${type}/popular`, {
|
const response = await axios.get(`${BASE_URL}/${type}/popular`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
page,
|
page,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -1501,9 +1435,10 @@ export class TMDBService {
|
||||||
* Get upcoming/now playing content
|
* Get upcoming/now playing content
|
||||||
* @param type 'movie' or 'tv'
|
* @param type 'movie' or 'tv'
|
||||||
* @param page Page number for pagination
|
* @param page Page number for pagination
|
||||||
|
* @param language Language for localized results
|
||||||
*/
|
*/
|
||||||
async getUpcoming(type: 'movie' | 'tv', page: number = 1): Promise<TMDBTrendingResult[]> {
|
async getUpcoming(type: 'movie' | 'tv', page: number = 1, language: string = 'en-US'): Promise<TMDBTrendingResult[]> {
|
||||||
const cacheKey = this.generateCacheKey(`upcoming_${type}`, { page });
|
const cacheKey = this.generateCacheKey(`upcoming_${type}`, { page, language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
|
||||||
|
|
@ -1516,7 +1451,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/${type}/${endpoint}`, {
|
const response = await axios.get(`${BASE_URL}/${type}/${endpoint}`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
page,
|
page,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -1554,9 +1489,10 @@ export class TMDBService {
|
||||||
* Get now playing movies (currently in theaters)
|
* Get now playing movies (currently in theaters)
|
||||||
* @param page Page number for pagination
|
* @param page Page number for pagination
|
||||||
* @param region ISO 3166-1 country code (e.g., 'US', 'GB')
|
* @param region ISO 3166-1 country code (e.g., 'US', 'GB')
|
||||||
|
* @param language Language for localized results
|
||||||
*/
|
*/
|
||||||
async getNowPlaying(page: number = 1, region: string = 'US'): Promise<TMDBTrendingResult[]> {
|
async getNowPlaying(page: number = 1, region: string = 'US', language: string = 'en-US'): Promise<TMDBTrendingResult[]> {
|
||||||
const cacheKey = this.generateCacheKey('now_playing', { page, region });
|
const cacheKey = this.generateCacheKey('now_playing', { page, region, language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
|
||||||
|
|
@ -1566,7 +1502,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/movie/now_playing`, {
|
const response = await axios.get(`${BASE_URL}/movie/now_playing`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
page,
|
page,
|
||||||
region, // Filter by region to get accurate theater availability
|
region, // Filter by region to get accurate theater availability
|
||||||
}),
|
}),
|
||||||
|
|
@ -1603,9 +1539,10 @@ export class TMDBService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of official movie genres from TMDB
|
* Get the list of official movie genres from TMDB
|
||||||
|
* @param language Language for localized genre names
|
||||||
*/
|
*/
|
||||||
async getMovieGenres(): Promise<{ id: number; name: string }[]> {
|
async getMovieGenres(language: string = 'en-US'): Promise<{ id: number; name: string }[]> {
|
||||||
const cacheKey = this.generateCacheKey('genres_movie');
|
const cacheKey = this.generateCacheKey('genres_movie', { language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<{ id: number; name: string }[]>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<{ id: number; name: string }[]>(cacheKey);
|
||||||
|
|
@ -1615,7 +1552,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/genre/movie/list`, {
|
const response = await axios.get(`${BASE_URL}/genre/movie/list`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = response.data.genres || [];
|
const data = response.data.genres || [];
|
||||||
|
|
@ -1628,9 +1565,10 @@ export class TMDBService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of official TV genres from TMDB
|
* Get the list of official TV genres from TMDB
|
||||||
|
* @param language Language for localized genre names
|
||||||
*/
|
*/
|
||||||
async getTvGenres(): Promise<{ id: number; name: string }[]> {
|
async getTvGenres(language: string = 'en-US'): Promise<{ id: number; name: string }[]> {
|
||||||
const cacheKey = this.generateCacheKey('genres_tv');
|
const cacheKey = this.generateCacheKey('genres_tv', { language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<{ id: number; name: string }[]>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<{ id: number; name: string }[]>(cacheKey);
|
||||||
|
|
@ -1640,7 +1578,7 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/genre/tv/list`, {
|
const response = await axios.get(`${BASE_URL}/genre/tv/list`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = response.data.genres || [];
|
const data = response.data.genres || [];
|
||||||
|
|
@ -1656,9 +1594,10 @@ export class TMDBService {
|
||||||
* @param type 'movie' or 'tv'
|
* @param type 'movie' or 'tv'
|
||||||
* @param genreName The genre name to filter by
|
* @param genreName The genre name to filter by
|
||||||
* @param page Page number for pagination
|
* @param page Page number for pagination
|
||||||
|
* @param language Language for localized results
|
||||||
*/
|
*/
|
||||||
async discoverByGenre(type: 'movie' | 'tv', genreName: string, page: number = 1): Promise<TMDBTrendingResult[]> {
|
async discoverByGenre(type: 'movie' | 'tv', genreName: string, page: number = 1, language: string = 'en-US'): Promise<TMDBTrendingResult[]> {
|
||||||
const cacheKey = this.generateCacheKey(`discover_${type}`, { genreName, page });
|
const cacheKey = this.generateCacheKey(`discover_${type}`, { genreName, page, language });
|
||||||
|
|
||||||
// Check cache (local or remote)
|
// Check cache (local or remote)
|
||||||
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
|
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
|
||||||
|
|
@ -1667,8 +1606,8 @@ export class TMDBService {
|
||||||
try {
|
try {
|
||||||
// First get the genre ID from the name
|
// First get the genre ID from the name
|
||||||
const genreList = type === 'movie'
|
const genreList = type === 'movie'
|
||||||
? await this.getMovieGenres()
|
? await this.getMovieGenres(language)
|
||||||
: await this.getTvGenres();
|
: await this.getTvGenres(language);
|
||||||
|
|
||||||
const genre = genreList.find(g => g.name.toLowerCase() === genreName.toLowerCase());
|
const genre = genreList.find(g => g.name.toLowerCase() === genreName.toLowerCase());
|
||||||
|
|
||||||
|
|
@ -1679,13 +1618,12 @@ export class TMDBService {
|
||||||
const response = await axios.get(`${BASE_URL}/discover/${type}`, {
|
const response = await axios.get(`${BASE_URL}/discover/${type}`, {
|
||||||
headers: await this.getHeaders(),
|
headers: await this.getHeaders(),
|
||||||
params: await this.getParams({
|
params: await this.getParams({
|
||||||
language: 'en-US',
|
language,
|
||||||
sort_by: 'popularity.desc',
|
sort_by: 'popularity.desc',
|
||||||
include_adult: false,
|
include_adult: false,
|
||||||
include_video: false,
|
include_video: false,
|
||||||
page,
|
page,
|
||||||
with_genres: genre.id.toString(),
|
with_genres: genre.id.toString(),
|
||||||
with_original_language: 'en',
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -577,6 +577,10 @@ export type TraktContentCommentLegacy =
|
||||||
| TraktEpisodeComment
|
| TraktEpisodeComment
|
||||||
| TraktListComment;
|
| TraktListComment;
|
||||||
|
|
||||||
|
|
||||||
|
const TRAKT_MAINTENANCE_MODE = true;
|
||||||
|
const TRAKT_MAINTENANCE_MESSAGE = 'Trakt integration is temporarily unavailable for maintenance. Please try again later.';
|
||||||
|
|
||||||
export class TraktService {
|
export class TraktService {
|
||||||
private static instance: TraktService;
|
private static instance: TraktService;
|
||||||
private accessToken: string | null = null;
|
private accessToken: string | null = null;
|
||||||
|
|
@ -584,6 +588,16 @@ export class TraktService {
|
||||||
private tokenExpiry: number = 0;
|
private tokenExpiry: number = 0;
|
||||||
private isInitialized: boolean = false;
|
private isInitialized: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
|
public isMaintenanceMode(): boolean {
|
||||||
|
return TRAKT_MAINTENANCE_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public getMaintenanceMessage(): string {
|
||||||
|
return TRAKT_MAINTENANCE_MESSAGE;
|
||||||
|
}
|
||||||
|
|
||||||
// Rate limiting - Optimized for real-time scrobbling
|
// Rate limiting - Optimized for real-time scrobbling
|
||||||
private lastApiCall: number = 0;
|
private lastApiCall: number = 0;
|
||||||
private readonly MIN_API_INTERVAL = 500; // Reduced to 500ms for faster updates
|
private readonly MIN_API_INTERVAL = 500; // Reduced to 500ms for faster updates
|
||||||
|
|
@ -726,6 +740,12 @@ export class TraktService {
|
||||||
* Check if the user is authenticated with Trakt
|
* Check if the user is authenticated with Trakt
|
||||||
*/
|
*/
|
||||||
public async isAuthenticated(): Promise<boolean> {
|
public async isAuthenticated(): Promise<boolean> {
|
||||||
|
// During maintenance, report as not authenticated to disable all syncing
|
||||||
|
if (this.isMaintenanceMode()) {
|
||||||
|
logger.log('[TraktService] Maintenance mode: reporting as not authenticated');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
if (!this.accessToken) {
|
if (!this.accessToken) {
|
||||||
|
|
@ -756,6 +776,12 @@ export class TraktService {
|
||||||
* Exchange the authorization code for an access token
|
* Exchange the authorization code for an access token
|
||||||
*/
|
*/
|
||||||
public async exchangeCodeForToken(code: string, codeVerifier: string): Promise<boolean> {
|
public async exchangeCodeForToken(code: string, codeVerifier: string): Promise<boolean> {
|
||||||
|
// Block authentication during maintenance
|
||||||
|
if (this.isMaintenanceMode()) {
|
||||||
|
logger.warn('[TraktService] Maintenance mode: blocking new authentication');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -887,6 +913,12 @@ export class TraktService {
|
||||||
body?: any,
|
body?: any,
|
||||||
retryCount: number = 0
|
retryCount: number = 0
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
// Block all API requests during maintenance
|
||||||
|
if (this.isMaintenanceMode()) {
|
||||||
|
logger.warn('[TraktService] Maintenance mode: blocking API request to', endpoint);
|
||||||
|
throw new Error(TRAKT_MAINTENANCE_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
// Rate limiting: ensure minimum interval between API calls
|
// Rate limiting: ensure minimum interval between API calls
|
||||||
|
|
@ -1106,10 +1138,10 @@ export class TraktService {
|
||||||
? imdbId
|
? imdbId
|
||||||
: `tt${imdbId}`;
|
: `tt${imdbId}`;
|
||||||
|
|
||||||
const response = await this.client.get('/sync/watched/movies');
|
const movies = await this.apiRequest<any[]>('/sync/watched/movies');
|
||||||
const movies = Array.isArray(response.data) ? response.data : [];
|
const moviesArray = Array.isArray(movies) ? movies : [];
|
||||||
|
|
||||||
return movies.some(
|
return moviesArray.some(
|
||||||
(m: any) => m.movie?.ids?.imdb === imdb
|
(m: any) => m.movie?.ids?.imdb === imdb
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue