mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
Added TMDB Multilang Support
This commit is contained in:
parent
2a89695b0b
commit
3220e91f1c
5 changed files with 501 additions and 235 deletions
|
|
@ -110,7 +110,7 @@ interface UseMetadataReturn {
|
|||
}
|
||||
|
||||
export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadataReturn => {
|
||||
const { settings } = useSettings();
|
||||
const { settings, isLoaded: settingsLoaded } = useSettings();
|
||||
const [metadata, setMetadata] = useState<StreamingContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -421,7 +421,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// For TMDB IDs, we need to handle metadata differently
|
||||
if (type === 'movie') {
|
||||
if (__DEV__) logger.log('Fetching movie details from TMDB for:', tmdbId);
|
||||
const movieDetails = await tmdbService.getMovieDetails(tmdbId);
|
||||
const movieDetails = await tmdbService.getMovieDetails(
|
||||
tmdbId,
|
||||
settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}-US` : 'en-US'
|
||||
);
|
||||
if (movieDetails) {
|
||||
const imdbId = movieDetails.imdb_id || movieDetails.external_ids?.imdb_id;
|
||||
if (imdbId) {
|
||||
|
|
@ -485,7 +488,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// Handle TV shows with TMDB IDs
|
||||
if (__DEV__) logger.log('Fetching TV show details from TMDB for:', tmdbId);
|
||||
try {
|
||||
const showDetails = await tmdbService.getTVShowDetails(parseInt(tmdbId));
|
||||
const showDetails = await tmdbService.getTVShowDetails(
|
||||
parseInt(tmdbId),
|
||||
settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}-US` : 'en-US'
|
||||
);
|
||||
if (showDetails) {
|
||||
// Get external IDs to check for IMDb ID
|
||||
const externalIds = await tmdbService.getShowExternalIds(parseInt(tmdbId));
|
||||
|
|
@ -587,16 +593,52 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
|
||||
if (content.status === 'fulfilled' && content.value) {
|
||||
if (__DEV__) logger.log('[loadMetadata] addon metadata:success', { id: content.value?.id, type: content.value?.type, name: content.value?.name });
|
||||
setMetadata(content.value);
|
||||
// Check if item is in library
|
||||
|
||||
// Start with addon metadata
|
||||
let finalMetadata = content.value as StreamingContent;
|
||||
|
||||
// If localization is enabled, merge TMDB localized text (name/overview) before first render
|
||||
try {
|
||||
if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) {
|
||||
const tmdbSvc = TMDBService.getInstance();
|
||||
// Ensure we have a TMDB ID
|
||||
let finalTmdbId: number | null = tmdbId;
|
||||
if (!finalTmdbId) {
|
||||
finalTmdbId = await tmdbSvc.extractTMDBIdFromStremioId(actualId);
|
||||
if (finalTmdbId) setTmdbId(finalTmdbId);
|
||||
}
|
||||
if (finalTmdbId) {
|
||||
const lang = settings.tmdbLanguagePreference || 'en';
|
||||
if (type === 'movie') {
|
||||
const localized = await tmdbSvc.getMovieDetails(String(finalTmdbId), lang);
|
||||
if (localized) {
|
||||
finalMetadata = {
|
||||
...finalMetadata,
|
||||
name: localized.title || finalMetadata.name,
|
||||
description: localized.overview || finalMetadata.description,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const localized = await tmdbSvc.getTVShowDetails(Number(finalTmdbId), lang);
|
||||
if (localized) {
|
||||
finalMetadata = {
|
||||
...finalMetadata,
|
||||
name: localized.name || finalMetadata.name,
|
||||
description: localized.overview || finalMetadata.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (__DEV__) console.log('[useMetadata] failed to merge localized TMDB text', e);
|
||||
}
|
||||
|
||||
// Commit final metadata once and cache it
|
||||
setMetadata(finalMetadata);
|
||||
cacheService.setMetadata(id, type, finalMetadata);
|
||||
const isInLib = catalogService.getLibraryItems().some(item => item.id === id);
|
||||
setInLibrary(isInLib);
|
||||
cacheService.setMetadata(id, type, content.value);
|
||||
|
||||
// Set the final metadata state without fetching logo (this will be handled by MetadataScreen)
|
||||
setMetadata(content.value);
|
||||
// Update cache
|
||||
cacheService.setMetadata(id, type, content.value);
|
||||
} else {
|
||||
if (__DEV__) logger.warn('[loadMetadata] addon metadata:not found or failed', { status: content.status, reason: (content as any)?.reason?.message });
|
||||
throw new Error('Content not found');
|
||||
|
|
@ -693,6 +735,40 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
if (__DEV__) logger.log('[loadSeriesData] TMDB enrichment disabled; skipping season poster fetch');
|
||||
}
|
||||
|
||||
// If localized TMDB text is enabled, merge episode names/overviews per language
|
||||
if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) {
|
||||
try {
|
||||
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
|
||||
if (tmdbIdToUse) {
|
||||
const lang = `${settings.tmdbLanguagePreference || 'en'}-US`;
|
||||
const seasons = Object.keys(groupedAddonEpisodes).map(Number);
|
||||
for (const seasonNum of seasons) {
|
||||
const seasonEps = groupedAddonEpisodes[seasonNum];
|
||||
// Parallel fetch a reasonable batch (limit concurrency implicitly by season)
|
||||
const localized = await Promise.all(
|
||||
seasonEps.map(async ep => {
|
||||
try {
|
||||
const data = await tmdbService.getEpisodeDetails(Number(tmdbIdToUse), seasonNum, ep.episode_number, lang);
|
||||
if (data) {
|
||||
return {
|
||||
...ep,
|
||||
name: data.name || ep.name,
|
||||
overview: data.overview || ep.overview,
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
return ep;
|
||||
})
|
||||
);
|
||||
groupedAddonEpisodes[seasonNum] = localized;
|
||||
}
|
||||
if (__DEV__) logger.log('[useMetadata] merged localized episode names/overviews from TMDB');
|
||||
}
|
||||
} catch (e) {
|
||||
if (__DEV__) console.log('[useMetadata] failed to merge localized episode text', e);
|
||||
}
|
||||
}
|
||||
|
||||
setGroupedEpisodes(groupedAddonEpisodes);
|
||||
|
||||
// Determine initial season only once per series
|
||||
|
|
@ -1242,8 +1318,17 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
}, [error, loadAttempts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!settingsLoaded) return;
|
||||
loadMetadata();
|
||||
}, [id, type]);
|
||||
}, [id, type, settingsLoaded]);
|
||||
|
||||
// Re-fetch when localization settings change to guarantee selected language at open
|
||||
useEffect(() => {
|
||||
if (!settingsLoaded) return;
|
||||
if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) {
|
||||
loadMetadata();
|
||||
}
|
||||
}, [settingsLoaded, settings.enrichMetadataWithTMDB, settings.useTmdbLocalizedMetadata, settings.tmdbLanguagePreference]);
|
||||
|
||||
// Re-run series data loading when metadata updates with videos
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export interface AppSettings {
|
|||
aiChatEnabled: boolean; // Enable/disable Ask AI and AI features
|
||||
// Metadata enrichment
|
||||
enrichMetadataWithTMDB: boolean; // Use TMDB to enrich metadata (cast, certification, posters, fallbacks)
|
||||
useTmdbLocalizedMetadata: boolean; // Use TMDB localized metadata (titles, overviews) per tmdbLanguagePreference
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AppSettings = {
|
||||
|
|
@ -128,6 +129,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||
aiChatEnabled: false,
|
||||
// Metadata enrichment
|
||||
enrichMetadataWithTMDB: true,
|
||||
useTmdbLocalizedMetadata: false,
|
||||
};
|
||||
|
||||
const SETTINGS_STORAGE_KEY = 'app_settings';
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ import CustomAlert from '../components/CustomAlert';
|
|||
// TMDB API key - since the default key might be private in the service, we'll use our own
|
||||
const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c';
|
||||
|
||||
// Extra TMDB logo languages to always offer (only Arabic per request)
|
||||
const COMMON_TMDB_LANGUAGES: string[] = ['ar'];
|
||||
|
||||
// Define example shows with their IMDB IDs and TMDB IDs
|
||||
const EXAMPLE_SHOWS = [
|
||||
{
|
||||
|
|
@ -407,6 +410,9 @@ const LogoSourceSettings = () => {
|
|||
const [tmdbBanner, setTmdbBanner] = useState<string | null>(null);
|
||||
const [metahubBanner, setMetahubBanner] = useState<string | null>(null);
|
||||
const [loadingLogos, setLoadingLogos] = useState(true);
|
||||
// Track which language the preview is actually using and if it is a fallback
|
||||
const [previewLanguage, setPreviewLanguage] = useState<string>('');
|
||||
const [isPreviewFallback, setIsPreviewFallback] = useState<boolean>(false);
|
||||
|
||||
// State for TMDB language selection
|
||||
// Store unique language codes as strings
|
||||
|
|
@ -471,6 +477,7 @@ const LogoSourceSettings = () => {
|
|||
initialLogoPath = preferredLogo.file_path;
|
||||
initialLanguage = preferredTmdbLanguage;
|
||||
logger.log(`[LogoSourceSettings] Found initial ${preferredTmdbLanguage} TMDB logo for ${show.name}`);
|
||||
setIsPreviewFallback(false);
|
||||
} else {
|
||||
// Fallback to English logo
|
||||
const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === 'en');
|
||||
|
|
@ -479,22 +486,27 @@ const LogoSourceSettings = () => {
|
|||
initialLogoPath = englishLogo.file_path;
|
||||
initialLanguage = 'en';
|
||||
logger.log(`[LogoSourceSettings] Found initial English TMDB logo for ${show.name}`);
|
||||
setIsPreviewFallback(true);
|
||||
} else if (imagesData.logos[0]) {
|
||||
// Fallback to the first available logo
|
||||
initialLogoPath = imagesData.logos[0].file_path;
|
||||
initialLanguage = imagesData.logos[0].iso_639_1;
|
||||
logger.log(`[LogoSourceSettings] No English logo, using first available (${initialLanguage}) TMDB logo for ${show.name}`);
|
||||
setIsPreviewFallback(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (initialLogoPath) {
|
||||
setTmdbLogo(`https://image.tmdb.org/t/p/original${initialLogoPath}`);
|
||||
setPreviewLanguage(initialLanguage || '');
|
||||
} else {
|
||||
logger.warn(`[LogoSourceSettings] No valid initial TMDB logo found for ${show.name}`);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`[LogoSourceSettings] No TMDB logos found in response for ${show.name}`);
|
||||
setUniqueTmdbLanguages([]); // Ensure it's empty if no logos
|
||||
setPreviewLanguage('');
|
||||
setIsPreviewFallback(false);
|
||||
}
|
||||
|
||||
// Get TMDB banner (backdrop)
|
||||
|
|
@ -603,8 +615,24 @@ const LogoSourceSettings = () => {
|
|||
if (selectedLogoData) {
|
||||
setTmdbLogo(`https://image.tmdb.org/t/p/original${selectedLogoData.file_path}`);
|
||||
logger.log(`[LogoSourceSettings] Switched TMDB logo preview to language: ${languageCode}`);
|
||||
setPreviewLanguage(languageCode);
|
||||
setIsPreviewFallback(false);
|
||||
} else {
|
||||
logger.warn(`[LogoSourceSettings] Could not find logo data for selected language: ${languageCode}`);
|
||||
// Fallback to English, then first available if English is not present
|
||||
const englishData = tmdbLogosData.find(logo => logo.iso_639_1 === 'en');
|
||||
if (englishData) {
|
||||
setTmdbLogo(`https://image.tmdb.org/t/p/original${englishData.file_path}`);
|
||||
setPreviewLanguage('en');
|
||||
setIsPreviewFallback(true);
|
||||
} else if (tmdbLogosData[0]) {
|
||||
setTmdbLogo(`https://image.tmdb.org/t/p/original${tmdbLogosData[0].file_path}`);
|
||||
setPreviewLanguage(tmdbLogosData[0].iso_639_1 || '');
|
||||
setIsPreviewFallback(true);
|
||||
} else {
|
||||
setPreviewLanguage('');
|
||||
setIsPreviewFallback(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -833,15 +861,18 @@ const LogoSourceSettings = () => {
|
|||
<View style={styles.exampleContainer}>
|
||||
<Text style={styles.exampleLabel}>Example:</Text>
|
||||
{renderLogoExample(tmdbLogo, tmdbBanner, loadingLogos)}
|
||||
<Text style={styles.logoSourceLabel}>
|
||||
{`Preview language: ${(previewLanguage || '').toUpperCase() || 'N/A'}${isPreviewFallback ? ' (fallback)' : ''}`}
|
||||
</Text>
|
||||
<Text style={styles.logoSourceLabel}>{selectedShow.name} logo from TMDB</Text>
|
||||
</View>
|
||||
|
||||
{/* TMDB Language Selector */}
|
||||
{uniqueTmdbLanguages.length > 1 && (
|
||||
{true && (
|
||||
<View style={styles.languageSelectorContainer}>
|
||||
<Text style={styles.languageSelectorTitle}>Logo Language</Text>
|
||||
<Text style={styles.languageSelectorDescription}>
|
||||
Select your preferred language for TMDB logos.
|
||||
Select your preferred language for TMDB logos (includes common languages like Arabic even if not shown in this preview).
|
||||
</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
|
|
@ -850,8 +881,8 @@ const LogoSourceSettings = () => {
|
|||
scrollEventThrottle={32}
|
||||
decelerationRate="normal"
|
||||
>
|
||||
{/* Iterate over unique language codes */}
|
||||
{uniqueTmdbLanguages.map((langCode) => (
|
||||
{/* Merge unique languages from TMDB with a common list to ensure wider options */}
|
||||
{Array.from(new Set<string>([...uniqueTmdbLanguages, ...COMMON_TMDB_LANGUAGES])).map((langCode) => (
|
||||
<TouchableOpacity
|
||||
key={langCode} // Use the unique code as key
|
||||
style={[
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
Image,
|
||||
KeyboardAvoidingView,
|
||||
TouchableWithoutFeedback,
|
||||
Modal,
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
|
|
@ -50,6 +51,8 @@ const TMDBSettingsScreen = () => {
|
|||
const { currentTheme } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { settings, updateSetting } = useSettings();
|
||||
const [languagePickerVisible, setLanguagePickerVisible] = useState(false);
|
||||
const [languageSearch, setLanguageSearch] = useState('');
|
||||
|
||||
const openAlert = (
|
||||
title: string,
|
||||
|
|
@ -284,165 +287,311 @@ const TMDBSettingsScreen = () => {
|
|||
</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.switchCard, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
<View style={styles.switchTextContainer}>
|
||||
<Text style={[styles.switchTitle, { color: currentTheme.colors.text }]}>Enrich Metadata with TMDb</Text>
|
||||
<Text style={[styles.switchDescription, { color: currentTheme.colors.mediumEmphasis }]}>When enabled, the app augments addon metadata with TMDb for cast, certification, logos/posters, and episode fallback. Disable to strictly use addon metadata only.</Text>
|
||||
{/* Metadata Enrichment Section */}
|
||||
<View style={[styles.sectionCard, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<MaterialIcons name="movie" size={20} color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>Metadata Enrichment</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={settings.enrichMetadataWithTMDB}
|
||||
onValueChange={(v) => updateSetting('enrichMetadataWithTMDB', v)}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? (settings.enrichMetadataWithTMDB ? currentTheme.colors.white : currentTheme.colors.white) : ''}
|
||||
ios_backgroundColor={'rgba(255,255,255,0.1)'}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.switchCard, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
<View style={styles.switchTextContainer}>
|
||||
<Text style={[styles.switchTitle, { color: currentTheme.colors.text }]}>Use Custom TMDb API Key</Text>
|
||||
<Text style={[styles.switchDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Enable to use your own TMDb API key instead of the built-in one.
|
||||
Using your own API key may provide better performance and higher rate limits.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={useCustomKey}
|
||||
onValueChange={toggleUseCustomKey}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? (useCustomKey ? currentTheme.colors.white : currentTheme.colors.white) : ''}
|
||||
ios_backgroundColor={'rgba(255,255,255,0.1)'}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[styles.sectionDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Enhance your content metadata with TMDb data for better details and information.
|
||||
</Text>
|
||||
|
||||
{useCustomKey && (
|
||||
<>
|
||||
<View style={[styles.statusCard, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
<MaterialIcons
|
||||
name={isKeySet ? "check-circle" : "error-outline"}
|
||||
size={28}
|
||||
color={isKeySet ? currentTheme.colors.success : currentTheme.colors.warning}
|
||||
style={styles.statusIconContainer}
|
||||
/>
|
||||
<View style={styles.statusTextContainer}>
|
||||
<Text style={[styles.statusTitle, { color: currentTheme.colors.text }]}>
|
||||
{isKeySet ? "API Key Active" : "API Key Required"}
|
||||
</Text>
|
||||
<Text style={[styles.statusDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{isKeySet
|
||||
? "Your custom TMDb API key is set and active."
|
||||
: "Add your TMDb API key below."}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.text }]}>API Key</Text>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
ref={apiKeyInputRef}
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.elevation1,
|
||||
color: currentTheme.colors.text,
|
||||
borderColor: isInputFocused ? currentTheme.colors.primary : 'transparent'
|
||||
}
|
||||
]}
|
||||
value={apiKey}
|
||||
onChangeText={(text) => {
|
||||
setApiKey(text);
|
||||
if (testResult) setTestResult(null);
|
||||
}}
|
||||
placeholder="Paste your TMDb API key (v3)"
|
||||
placeholderTextColor={currentTheme.colors.mediumEmphasis}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
spellCheck={false}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.pasteButton}
|
||||
onPress={pasteFromClipboard}
|
||||
>
|
||||
<MaterialIcons name="content-paste" size={20} color={currentTheme.colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={saveApiKey}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: currentTheme.colors.white }]}>Save API Key</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{isKeySet && (
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.clearButton, { borderColor: currentTheme.colors.error }]}
|
||||
onPress={clearApiKey}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: currentTheme.colors.error }]}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{testResult && (
|
||||
<View style={[
|
||||
styles.resultMessage,
|
||||
{ backgroundColor: testResult.success ? currentTheme.colors.success + '1A' : currentTheme.colors.error + '1A' }
|
||||
]}>
|
||||
<MaterialIcons
|
||||
name={testResult.success ? "check-circle" : "error"}
|
||||
size={18}
|
||||
color={testResult.success ? currentTheme.colors.success : currentTheme.colors.error}
|
||||
style={styles.resultIcon}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.resultText,
|
||||
{ color: testResult.success ? currentTheme.colors.success : currentTheme.colors.error }
|
||||
]}>
|
||||
{testResult.message}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.helpLink}
|
||||
onPress={openTMDBWebsite}
|
||||
>
|
||||
<MaterialIcons name="help" size={16} color={currentTheme.colors.primary} style={styles.helpIcon} />
|
||||
<Text style={[styles.helpText, { color: currentTheme.colors.primary }]}>
|
||||
How to get a TMDb API key?
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={[styles.infoCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} />
|
||||
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
To get your own TMDb API key (v3), you need to create a TMDb account and request an API key from their website.
|
||||
Using your own API key gives you dedicated quota and may improve app performance.
|
||||
<View style={styles.settingRow}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Enable Enrichment</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Augments addon metadata with TMDb for cast, certification, logos/posters, and episode fallback.
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!useCustomKey && (
|
||||
<View style={[styles.infoCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} />
|
||||
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Currently using the built-in TMDb API key. This key is shared among all users.
|
||||
For better performance and reliability, consider using your own API key.
|
||||
</Text>
|
||||
<Switch
|
||||
value={settings.enrichMetadataWithTMDB}
|
||||
onValueChange={(v) => updateSetting('enrichMetadataWithTMDB', v)}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? (settings.enrichMetadataWithTMDB ? currentTheme.colors.white : currentTheme.colors.white) : ''}
|
||||
ios_backgroundColor={'rgba(255,255,255,0.1)'}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{settings.enrichMetadataWithTMDB && (
|
||||
<>
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={styles.settingRow}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Localized Text</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Fetch titles and descriptions in your preferred language from TMDb.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={settings.useTmdbLocalizedMetadata}
|
||||
onValueChange={(v) => updateSetting('useTmdbLocalizedMetadata', v)}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? (settings.useTmdbLocalizedMetadata ? currentTheme.colors.white : currentTheme.colors.white) : ''}
|
||||
ios_backgroundColor={'rgba(255,255,255,0.1)'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{settings.useTmdbLocalizedMetadata && (
|
||||
<>
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={styles.settingRow}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Language</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Current: {(settings.tmdbLanguagePreference || 'en').toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => setLanguagePickerVisible(true)}
|
||||
style={[styles.languageButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
>
|
||||
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* API Configuration Section */}
|
||||
<View style={[styles.sectionCard, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<MaterialIcons name="api" size={20} color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>API Configuration</Text>
|
||||
</View>
|
||||
<Text style={[styles.sectionDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Configure your TMDb API access for enhanced functionality.
|
||||
</Text>
|
||||
|
||||
<View style={styles.settingRow}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Custom API Key</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Use your own TMDb API key for better performance and dedicated rate limits.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={useCustomKey}
|
||||
onValueChange={toggleUseCustomKey}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? (useCustomKey ? currentTheme.colors.white : currentTheme.colors.white) : ''}
|
||||
ios_backgroundColor={'rgba(255,255,255,0.1)'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{useCustomKey && (
|
||||
<>
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* API Key Status */}
|
||||
<View style={styles.statusRow}>
|
||||
<MaterialIcons
|
||||
name={isKeySet ? "check-circle" : "error-outline"}
|
||||
size={20}
|
||||
color={isKeySet ? currentTheme.colors.success : currentTheme.colors.warning}
|
||||
/>
|
||||
<Text style={[styles.statusText, {
|
||||
color: isKeySet ? currentTheme.colors.success : currentTheme.colors.warning
|
||||
}]}>
|
||||
{isKeySet ? "Custom API key active" : "API key required"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* API Key Input */}
|
||||
<View style={styles.apiKeyContainer}>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
ref={apiKeyInputRef}
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.elevation1,
|
||||
color: currentTheme.colors.text,
|
||||
borderColor: isInputFocused ? currentTheme.colors.primary : 'transparent'
|
||||
}
|
||||
]}
|
||||
value={apiKey}
|
||||
onChangeText={(text) => {
|
||||
setApiKey(text);
|
||||
if (testResult) setTestResult(null);
|
||||
}}
|
||||
placeholder="Paste your TMDb API key (v3)"
|
||||
placeholderTextColor={currentTheme.colors.mediumEmphasis}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
spellCheck={false}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.pasteButton}
|
||||
onPress={pasteFromClipboard}
|
||||
>
|
||||
<MaterialIcons name="content-paste" size={20} color={currentTheme.colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={saveApiKey}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: currentTheme.colors.white }]}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{isKeySet && (
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.clearButton, { borderColor: currentTheme.colors.error }]}
|
||||
onPress={clearApiKey}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: currentTheme.colors.error }]}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{testResult && (
|
||||
<View style={[
|
||||
styles.resultMessage,
|
||||
{ backgroundColor: testResult.success ? currentTheme.colors.success + '1A' : currentTheme.colors.error + '1A' }
|
||||
]}>
|
||||
<MaterialIcons
|
||||
name={testResult.success ? "check-circle" : "error"}
|
||||
size={16}
|
||||
color={testResult.success ? currentTheme.colors.success : currentTheme.colors.error}
|
||||
style={styles.resultIcon}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.resultText,
|
||||
{ color: testResult.success ? currentTheme.colors.success : currentTheme.colors.error }
|
||||
]}>
|
||||
{testResult.message}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.helpLink}
|
||||
onPress={openTMDBWebsite}
|
||||
>
|
||||
<MaterialIcons name="help" size={16} color={currentTheme.colors.primary} style={styles.helpIcon} />
|
||||
<Text style={[styles.helpText, { color: currentTheme.colors.primary }]}>
|
||||
How to get a TMDb API key?
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!useCustomKey && (
|
||||
<View style={styles.infoContainer}>
|
||||
<MaterialIcons name="info-outline" size={18} color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Currently using built-in API key. Consider using your own key for better performance.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Language Picker Modal */}
|
||||
<Modal
|
||||
visible={languagePickerVisible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setLanguagePickerVisible(false)}
|
||||
>
|
||||
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }}>
|
||||
<View style={{ backgroundColor: currentTheme.colors.darkBackground, borderTopLeftRadius: 16, borderTopRightRadius: 16, width: '100%', maxHeight: '80%', padding: 16 }}>
|
||||
<View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: currentTheme.colors.elevation3, alignSelf: 'center', marginBottom: 10 }} />
|
||||
<Text style={{ color: currentTheme.colors.text, fontSize: 18, fontWeight: '800', marginBottom: 12 }}>Select Language</Text>
|
||||
<View style={{ flexDirection: 'row', marginBottom: 12 }}>
|
||||
<View style={{ flex: 1, borderRadius: 10, backgroundColor: currentTheme.colors.elevation1, paddingHorizontal: 12, paddingVertical: Platform.OS === 'ios' ? 10 : 0 }}>
|
||||
<TextInput
|
||||
placeholder="Search language (e.g. Arabic)"
|
||||
placeholderTextColor={currentTheme.colors.mediumEmphasis}
|
||||
style={{ color: currentTheme.colors.text, paddingVertical: Platform.OS === 'ios' ? 0 : 8 }}
|
||||
value={languageSearch}
|
||||
onChangeText={setLanguageSearch}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setLanguageSearch('')} style={{ marginLeft: 8, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 10, backgroundColor: currentTheme.colors.elevation1 }}>
|
||||
<Text style={{ color: currentTheme.colors.text }}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{/* Most used quick chips */}
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 8 }} contentContainerStyle={{ paddingVertical: 2 }}>
|
||||
{['en','ar','es','fr','de','tr'].map(code => (
|
||||
<TouchableOpacity key={code} onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }} style={{ paddingHorizontal: 10, paddingVertical: 6, backgroundColor: settings.tmdbLanguagePreference === code ? currentTheme.colors.primary : currentTheme.colors.elevation1, borderRadius: 999, marginRight: 8 }}>
|
||||
<Text style={{ color: settings.tmdbLanguagePreference === code ? currentTheme.colors.white : currentTheme.colors.text }}>{code.toUpperCase()}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
<ScrollView style={{ maxHeight: '70%' }}>
|
||||
{[
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'ar', label: 'Arabic' },
|
||||
{ code: 'es', label: 'Spanish' },
|
||||
{ code: 'fr', label: 'French' },
|
||||
{ code: 'de', label: 'German' },
|
||||
{ code: 'it', label: 'Italian' },
|
||||
{ code: 'pt', label: 'Portuguese' },
|
||||
{ code: 'ru', label: 'Russian' },
|
||||
{ code: 'tr', label: 'Turkish' },
|
||||
{ code: 'ja', label: 'Japanese' },
|
||||
{ code: 'ko', label: 'Korean' },
|
||||
{ code: 'zh', label: 'Chinese' },
|
||||
{ code: 'hi', label: 'Hindi' },
|
||||
{ code: 'he', label: 'Hebrew' },
|
||||
{ code: 'id', label: 'Indonesian' },
|
||||
{ code: 'nl', label: 'Dutch' },
|
||||
{ code: 'sv', label: 'Swedish' },
|
||||
{ code: 'no', label: 'Norwegian' },
|
||||
{ code: 'da', label: 'Danish' },
|
||||
{ code: 'fi', label: 'Finnish' },
|
||||
{ code: 'pl', label: 'Polish' },
|
||||
{ code: 'cs', label: 'Czech' },
|
||||
{ code: 'ro', label: 'Romanian' },
|
||||
{ code: 'uk', label: 'Ukrainian' },
|
||||
{ code: 'vi', label: 'Vietnamese' },
|
||||
{ code: 'th', label: 'Thai' },
|
||||
]
|
||||
.filter(({ label, code }) =>
|
||||
(languageSearch || '').length === 0 ||
|
||||
label.toLowerCase().includes(languageSearch.toLowerCase()) || code.toLowerCase().includes(languageSearch.toLowerCase())
|
||||
)
|
||||
.map(({ code, label }) => (
|
||||
<TouchableOpacity
|
||||
key={code}
|
||||
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
|
||||
style={{ paddingVertical: 12, paddingHorizontal: 6, borderRadius: 10, backgroundColor: settings.tmdbLanguagePreference === code ? currentTheme.colors.elevation1 : 'transparent', marginBottom: 4 }}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ color: currentTheme.colors.text, fontSize: 16 }}>
|
||||
{label} ({code.toUpperCase()})
|
||||
</Text>
|
||||
{settings.tmdbLanguagePreference === code && (
|
||||
<MaterialIcons name="check-circle" size={20} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
<TouchableOpacity onPress={() => setLanguagePickerVisible(false)} style={{ marginTop: 12, paddingVertical: 12, alignItems: 'center', borderRadius: 10, backgroundColor: currentTheme.colors.primary }}>
|
||||
<Text style={{ color: currentTheme.colors.white, fontWeight: '700' }}>Done</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</ScrollView>
|
||||
<CustomAlert
|
||||
visible={alertVisible}
|
||||
|
|
@ -502,73 +651,86 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 16,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
switchCard: {
|
||||
sectionCard: {
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 20,
|
||||
padding: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
switchTextContainer: {
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
marginLeft: 8,
|
||||
},
|
||||
sectionDescription: {
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
marginBottom: 20,
|
||||
},
|
||||
settingRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 16,
|
||||
},
|
||||
settingTextContainer: {
|
||||
flex: 1,
|
||||
marginRight: 16,
|
||||
},
|
||||
switchTitle: {
|
||||
settingTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
},
|
||||
switchDescription: {
|
||||
settingDescription: {
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
opacity: 0.8,
|
||||
},
|
||||
statusCard: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
languageButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
statusIconContainer: {
|
||||
marginRight: 12,
|
||||
},
|
||||
statusTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
statusTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
},
|
||||
statusDescription: {
|
||||
languageButtonText: {
|
||||
fontSize: 14,
|
||||
opacity: 0.8,
|
||||
},
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
marginVertical: 16,
|
||||
},
|
||||
statusRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
marginLeft: 8,
|
||||
},
|
||||
apiKeyContainer: {
|
||||
marginTop: 16,
|
||||
},
|
||||
infoContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
marginTop: 16,
|
||||
padding: 12,
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 8,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
@ -640,27 +802,12 @@ const styles = StyleSheet.create({
|
|||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
infoCard: {
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
infoIcon: {
|
||||
marginRight: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 14,
|
||||
flex: 1,
|
||||
lineHeight: 20,
|
||||
opacity: 0.8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -160,12 +160,12 @@ export class TMDBService {
|
|||
/**
|
||||
* Get TV show details by TMDB ID
|
||||
*/
|
||||
async getTVShowDetails(tmdbId: number): Promise<TMDBShow | null> {
|
||||
async getTVShowDetails(tmdbId: number, language: string = 'en'): Promise<TMDBShow | null> {
|
||||
try {
|
||||
const response = await axios.get(`${BASE_URL}/tv/${tmdbId}`, {
|
||||
headers: await this.getHeaders(),
|
||||
params: await this.getParams({
|
||||
language: 'en-US',
|
||||
language,
|
||||
append_to_response: 'external_ids,credits,keywords' // Append external IDs, cast/crew, and keywords for AI context
|
||||
}),
|
||||
});
|
||||
|
|
@ -237,12 +237,12 @@ export class TMDBService {
|
|||
/**
|
||||
* Get season details including all episodes with IMDb ratings
|
||||
*/
|
||||
async getSeasonDetails(tmdbId: number, seasonNumber: number, showName?: string): Promise<TMDBSeason | null> {
|
||||
async getSeasonDetails(tmdbId: number, seasonNumber: number, showName?: string, language: string = 'en-US'): Promise<TMDBSeason | null> {
|
||||
try {
|
||||
const response = await axios.get(`${BASE_URL}/tv/${tmdbId}/season/${seasonNumber}`, {
|
||||
headers: await this.getHeaders(),
|
||||
params: await this.getParams({
|
||||
language: 'en-US',
|
||||
language,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -292,7 +292,8 @@ export class TMDBService {
|
|||
async getEpisodeDetails(
|
||||
tmdbId: number,
|
||||
seasonNumber: number,
|
||||
episodeNumber: number
|
||||
episodeNumber: number,
|
||||
language: string = 'en-US'
|
||||
): Promise<TMDBEpisode | null> {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
|
|
@ -300,7 +301,7 @@ export class TMDBService {
|
|||
{
|
||||
headers: await this.getHeaders(),
|
||||
params: await this.getParams({
|
||||
language: 'en-US',
|
||||
language,
|
||||
append_to_response: 'credits' // Include guest stars and crew for episode context
|
||||
}),
|
||||
}
|
||||
|
|
@ -546,14 +547,14 @@ export class TMDBService {
|
|||
}
|
||||
}
|
||||
|
||||
async getRecommendations(type: 'movie' | 'tv', tmdbId: string): Promise<any[]> {
|
||||
async getRecommendations(type: 'movie' | 'tv', tmdbId: string, language: string = 'en-US'): Promise<any[]> {
|
||||
if (!this.apiKey) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const response = await axios.get(`${BASE_URL}/${type}/${tmdbId}/recommendations`, {
|
||||
headers: await this.getHeaders(),
|
||||
params: await this.getParams({ language: 'en-US' })
|
||||
params: await this.getParams({ language })
|
||||
});
|
||||
return response.data.results || [];
|
||||
} catch (error) {
|
||||
|
|
@ -581,12 +582,12 @@ export class TMDBService {
|
|||
/**
|
||||
* Get movie details by TMDB ID
|
||||
*/
|
||||
async getMovieDetails(movieId: string): Promise<any> {
|
||||
async getMovieDetails(movieId: string, language: string = 'en'): Promise<any> {
|
||||
try {
|
||||
const response = await axios.get(`${BASE_URL}/movie/${movieId}`, {
|
||||
headers: await this.getHeaders(),
|
||||
params: await this.getParams({
|
||||
language: 'en-US',
|
||||
language,
|
||||
append_to_response: 'external_ids,credits,keywords,release_dates' // Include release dates for accurate availability
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue