From fb316d9f372c0026065565beb14534e86f445ff8 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 11 Oct 2025 01:06:37 +0530 Subject: [PATCH] added langauge filtering for plugnins --- App.tsx | 9 +- src/components/metadata/CastSection.tsx | 4 +- src/components/player/AndroidVideoPlayer.tsx | 8 + src/hooks/useMetadata.ts | 18 ++ src/hooks/useSettings.ts | 4 + src/screens/MetadataScreen.tsx | 1 + src/screens/PluginsScreen.tsx | 68 +++++ src/screens/StreamsScreen.tsx | 106 +++++++- src/services/backupService.ts | 260 +++++++++++++++++-- src/services/catalogService.ts | 30 ++- 10 files changed, 477 insertions(+), 31 deletions(-) diff --git a/App.tsx b/App.tsx index 038fb00..6cdab60 100644 --- a/App.tsx +++ b/App.tsx @@ -10,7 +10,8 @@ import { View, StyleSheet, I18nManager, - Platform + Platform, + LogBox } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; @@ -61,6 +62,12 @@ Sentry.init({ I18nManager.allowRTL(false); I18nManager.forceRTL(false); +// Suppress duplicate key warnings app-wide +LogBox.ignoreLogs([ + 'Warning: Encountered two children with the same key', + 'Keys should be unique so that components maintain their identity across updates' +]); + // This fixes many navigation layout issues by using native screen containers enableScreens(true); diff --git a/src/components/metadata/CastSection.tsx b/src/components/metadata/CastSection.tsx index 305f31e..20b034e 100644 --- a/src/components/metadata/CastSection.tsx +++ b/src/components/metadata/CastSection.tsx @@ -17,12 +17,14 @@ interface CastSectionProps { cast: any[]; loadingCast: boolean; onSelectCastMember: (castMember: any) => void; + isTmdbEnrichmentEnabled?: boolean; } export const CastSection: React.FC = ({ cast, loadingCast, onSelectCastMember, + isTmdbEnrichmentEnabled = true, }) => { const { currentTheme } = useTheme(); @@ -80,7 +82,7 @@ export const CastSection: React.FC = ({ )} {item.name} - {item.character && ( + {isTmdbEnrichmentEnabled && item.character && ( {item.character} )} diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index c64005d..ddffe64 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -2267,6 +2267,14 @@ const AndroidVideoPlayer: React.FC = () => { } }, [useVLC, selectVlcSubtitleTrack]); + // Automatically disable VLC internal subtitles when external subtitles are enabled + useEffect(() => { + if (useVLC && useCustomSubtitles) { + logger.log('[AndroidVideoPlayer][VLC] External subtitles enabled, disabling internal subtitles'); + selectVlcSubtitleTrack(null); + } + }, [useVLC, useCustomSubtitles, selectVlcSubtitleTrack]); + const disableCustomSubtitles = useCallback(() => { setUseCustomSubtitles(false); setCustomSubtitles([]); diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 04c4fc8..bb070dc 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -391,6 +391,16 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat try { if (!settings.enrichMetadataWithTMDB) { if (__DEV__) logger.log('[loadCast] TMDB enrichment disabled by settings'); + + // Check if we have addon cast data available + if (metadata?.addonCast && metadata.addonCast.length > 0) { + if (__DEV__) logger.log(`[loadCast] Using addon cast data: ${metadata.addonCast.length} cast members`); + setCast(metadata.addonCast); + setLoadingCast(false); + return; + } + + if (__DEV__) logger.log('[loadCast] No addon cast data available'); setLoadingCast(false); return; } @@ -1713,6 +1723,14 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } }, [tmdbId, loadRecommendations, settings.enrichMetadataWithTMDB]); + // Load addon cast data when metadata is available and TMDB enrichment is disabled + useEffect(() => { + if (!settings.enrichMetadataWithTMDB && metadata?.addonCast && metadata.addonCast.length > 0) { + if (__DEV__) logger.log('[useMetadata] Loading addon cast data after metadata loaded'); + loadCast(); + } + }, [metadata, settings.enrichMetadataWithTMDB]); + // Ensure certification is attached whenever a TMDB id is known and metadata lacks it useEffect(() => { const maybeAttachCertification = async () => { diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index dbddb01..7dc62db 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -55,6 +55,8 @@ export interface AppSettings { showScraperLogos: boolean; // Show scraper logos next to streaming links // Quality filtering settings excludedQualities: string[]; // Array of quality strings to exclude (e.g., ['2160p', '4K', '1080p', '720p']) + // Language filtering settings + excludedLanguages: string[]; // Array of language strings to exclude (e.g., ['Spanish', 'German', 'French']) // Playback behavior alwaysResume: boolean; // If true, resume automatically without prompt when progress < 85% // Downloads @@ -110,6 +112,8 @@ export const DEFAULT_SETTINGS: AppSettings = { showScraperLogos: true, // Show scraper logos by default // Quality filtering defaults excludedQualities: [], // No qualities excluded by default + // Language filtering defaults + excludedLanguages: [], // No languages excluded by default // Playback behavior defaults alwaysResume: true, // Downloads diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index cec3c37..d1640ac 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -858,6 +858,7 @@ const MetadataScreen: React.FC = () => { cast={cast} loadingCast={loadingCast} onSelectCastMember={handleSelectCastMember} + isTmdbEnrichmentEnabled={settings.enrichMetadataWithTMDB} /> )} diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx index 75053d6..e093991 100644 --- a/src/screens/PluginsScreen.tsx +++ b/src/screens/PluginsScreen.tsx @@ -1295,8 +1295,27 @@ const PluginsScreen: React.FC = () => { await updateSetting('excludedQualities', newExcluded); }; + const handleToggleLanguageExclusion = async (language: string) => { + const currentExcluded = settings.excludedLanguages || []; + const isExcluded = currentExcluded.includes(language); + + let newExcluded: string[]; + if (isExcluded) { + // Remove from excluded list + newExcluded = currentExcluded.filter(l => l !== language); + } else { + // Add to excluded list + newExcluded = [...currentExcluded, language]; + } + + await updateSetting('excludedLanguages', newExcluded); + }; + // Define available quality options const qualityOptions = ['Auto', 'Adaptive', '2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS']; + + // Define available language options + const languageOptions = ['Original', 'English', 'Spanish', 'Latin', 'French', 'German', 'Italian', 'Portuguese', 'Russian', 'Japanese', 'Korean', 'Chinese', 'Arabic', 'Hindi', 'Turkish', 'Dutch', 'Polish']; @@ -1815,6 +1834,55 @@ const PluginsScreen: React.FC = () => { )} + {/* Language Filtering */} + toggleSection('quality')} + colors={colors} + styles={styles} + > + + Exclude specific languages from search results. Tap on a language to exclude it from plugin results. + + + + Note: This filter only applies to providers that include language information in their stream names. It does not affect other providers. + + + + {languageOptions.map((language) => { + const isExcluded = (settings.excludedLanguages || []).includes(language); + return ( + handleToggleLanguageExclusion(language)} + disabled={!settings.enableLocalScrapers} + > + + {isExcluded ? 'βœ• ' : ''}{language} + + + ); + })} + + + {(settings.excludedLanguages || []).length > 0 && ( + + Excluded languages: {(settings.excludedLanguages || []).join(', ')} + + )} + + {/* About */} About Plugins diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index e19c1f3..5eb0889 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -878,6 +878,86 @@ export const StreamsScreen = () => { }); }, [settings.excludedQualities]); + // Helper function to filter streams by language exclusions + const filterStreamsByLanguage = useCallback((streams: Stream[]) => { + if (!settings.excludedLanguages || settings.excludedLanguages.length === 0) { + console.log('πŸ” [filterStreamsByLanguage] No excluded languages, returning all streams'); + return streams; + } + + console.log('πŸ” [filterStreamsByLanguage] Filtering with excluded languages:', settings.excludedLanguages); + + // Log first few stream details to see what fields contain language info + if (streams.length > 0) { + console.log('πŸ” [filterStreamsByLanguage] Sample stream details:', streams.slice(0, 3).map(s => ({ + title: s.title || s.name, + description: s.description?.substring(0, 100), + name: s.name, + addonName: s.addonName, + addonId: s.addonId + }))); + } + + const filtered = streams.filter(stream => { + const streamName = stream.name || ''; // This contains the language info like "VIDEASY Gekko (Latin) - Adaptive" + const streamTitle = stream.title || ''; + const streamDescription = stream.description || ''; + const searchText = `${streamName} ${streamTitle} ${streamDescription}`.toLowerCase(); + + // Check if any excluded language is found in the stream title or description + const hasExcludedLanguage = settings.excludedLanguages.some(excludedLanguage => { + const langLower = excludedLanguage.toLowerCase(); + + // Check multiple variations of the language name + const variations = [langLower]; + + // Add common variations for each language + if (langLower === 'latin') { + variations.push('latino', 'latina', 'lat'); + } else if (langLower === 'spanish') { + variations.push('espaΓ±ol', 'espanol', 'spa'); + } else if (langLower === 'german') { + variations.push('deutsch', 'ger'); + } else if (langLower === 'french') { + variations.push('franΓ§ais', 'francais', 'fre'); + } else if (langLower === 'portuguese') { + variations.push('portuguΓͺs', 'portugues', 'por'); + } else if (langLower === 'italian') { + variations.push('ita'); + } else if (langLower === 'english') { + variations.push('eng'); + } else if (langLower === 'japanese') { + variations.push('jap'); + } else if (langLower === 'korean') { + variations.push('kor'); + } else if (langLower === 'chinese') { + variations.push('chi', 'cn'); + } else if (langLower === 'arabic') { + variations.push('ara'); + } else if (langLower === 'russian') { + variations.push('rus'); + } else if (langLower === 'turkish') { + variations.push('tur'); + } else if (langLower === 'hindi') { + variations.push('hin'); + } + + const matches = variations.some(variant => searchText.includes(variant)); + + if (matches) { + console.log(`πŸ” [filterStreamsByLanguage] βœ• Excluding stream with ${excludedLanguage}:`, streamName.substring(0, 100)); + } + return matches; + }); + + // Return true to keep the stream (if it doesn't have excluded language) + return !hasExcludedLanguage; + }); + + console.log(`πŸ” [filterStreamsByLanguage] Filtered ${streams.length} β†’ ${filtered.length} streams`); + return filtered; + }, [settings.excludedLanguages]); + // Note: No additional sorting applied to stream cards; preserve provider order // Function to determine the best stream based on quality, provider priority, and other factors @@ -934,8 +1014,9 @@ export const StreamsScreen = () => { }> = []; Object.entries(streamsData).forEach(([addonId, { streams }]) => { - // Apply quality filtering to streams before processing - const filteredStreams = filterStreamsByQuality(streams); + // Apply quality and language filtering to streams before processing + const qualityFiltered = filterStreamsByQuality(streams); + const filteredStreams = filterStreamsByLanguage(qualityFiltered); filteredStreams.forEach(stream => { const quality = getQualityNumeric(stream.name || stream.title); @@ -1502,8 +1583,9 @@ export const StreamsScreen = () => { // For ADDONS: Keep all streams in original order, NO filtering or sorting addonStreams.push(...providerStreams); } else { - // For PLUGINS: Apply quality filtering and sorting - const filteredStreams = filterStreamsByQuality(providerStreams); + // For PLUGINS: Apply quality and language filtering and sorting + const qualityFiltered = filterStreamsByQuality(providerStreams); + const filteredStreams = filterStreamsByLanguage(qualityFiltered); if (filteredStreams.length > 0) { pluginStreams.push(...filteredStreams); @@ -1621,23 +1703,25 @@ export const StreamsScreen = () => { let filteredStreams = providerStreams; let isEmptyDueToQualityFilter = false; - // Only apply quality filtering to plugins, NOT addons + // Only apply quality and language filtering to plugins, NOT addons if (!isInstalledAddon) { - console.log('πŸ” [StreamsScreen] Applying quality filter to plugin:', { + console.log('πŸ” [StreamsScreen] Applying quality and language filters to plugin:', { addonId, addonName, originalCount, - excludedQualities: settings.excludedQualities + excludedQualities: settings.excludedQualities, + excludedLanguages: settings.excludedLanguages }); - filteredStreams = filterStreamsByQuality(providerStreams); + const qualityFiltered = filterStreamsByQuality(providerStreams); + filteredStreams = filterStreamsByLanguage(qualityFiltered); isEmptyDueToQualityFilter = originalCount > 0 && filteredStreams.length === 0; - console.log('πŸ” [StreamsScreen] Quality filter result:', { + console.log('πŸ” [StreamsScreen] Quality and language filter result:', { addonId, filteredCount: filteredStreams.length, isEmptyDueToQualityFilter }); } else { - console.log('πŸ” [StreamsScreen] Skipping quality filter for addon:', { + console.log('πŸ” [StreamsScreen] Skipping quality and language filters for addon:', { addonId, addonName, originalCount @@ -2081,7 +2165,7 @@ export const StreamsScreen = () => { data={section.data} keyExtractor={(item, index) => { if (item && item.url) { - return item.url; + return `${item.url}-${sectionIndex}-${index}`; } return `empty-${sectionIndex}-${index}`; }} diff --git a/src/services/backupService.ts b/src/services/backupService.ts index 15146db..bb6188c 100644 --- a/src/services/backupService.ts +++ b/src/services/backupService.ts @@ -20,7 +20,7 @@ export interface BackupData { downloads: DownloadItem[]; subtitles: any; tombstones: Record; - continueWatchingRemoved: string[]; + continueWatchingRemoved: Record; contentDuration: Record; syncQueue: any[]; traktSettings?: any; @@ -32,7 +32,19 @@ export interface BackupData { scraperSettings: any; scraperCode: Record; }; - customThemes?: any[]; + // API Keys + apiKeys?: { + mdblistApiKey?: string; + openRouterApiKey?: string; + }; + // User preferences + catalogSettings?: any; + addonOrder?: string[]; + removedAddons?: string[]; + globalSeasonViewMode?: string; + // Onboarding/flags + hasCompletedOnboarding?: boolean; + showLoginHintToastOnce?: boolean; }; metadata: { totalItems: number; @@ -52,6 +64,9 @@ export interface BackupOptions { includeSettings?: boolean; includeTraktData?: boolean; includeLocalScrapers?: boolean; + includeApiKeys?: boolean; + includeCatalogSettings?: boolean; + includeUserPreferences?: boolean; } export class BackupService { @@ -87,7 +102,7 @@ export class BackupService { platform: Platform.OS as 'ios' | 'android', userScope, data: { - settings: await this.getSettings(), + settings: options.includeSettings !== false ? await this.getSettings() : DEFAULT_SETTINGS, library: options.includeLibrary !== false ? await this.getLibrary() : [], watchProgress: options.includeWatchProgress !== false ? await this.getWatchProgress() : {}, addons: options.includeAddons !== false ? await this.getAddons() : [], @@ -99,6 +114,13 @@ export class BackupService { syncQueue: await this.getSyncQueue(), traktSettings: options.includeTraktData !== false ? await this.getTraktSettings() : undefined, localScrapers: options.includeLocalScrapers !== false ? await this.getLocalScrapers() : undefined, + apiKeys: options.includeApiKeys !== false ? await this.getApiKeys() : undefined, + catalogSettings: options.includeCatalogSettings !== false ? await this.getCatalogSettings() : undefined, + addonOrder: options.includeUserPreferences !== false ? await this.getAddonOrder() : undefined, + removedAddons: options.includeUserPreferences !== false ? await this.getRemovedAddons() : undefined, + globalSeasonViewMode: options.includeUserPreferences !== false ? await this.getGlobalSeasonViewMode() : undefined, + hasCompletedOnboarding: options.includeUserPreferences !== false ? await this.getHasCompletedOnboarding() : undefined, + showLoginHintToastOnce: options.includeUserPreferences !== false ? await this.getShowLoginHintToastOnce() : undefined, }, metadata: { totalItems: 0, @@ -205,23 +227,23 @@ export class BackupService { logger.info(`[BackupService] Backup contains: ${backupData.metadata.totalItems} items`); // Restore data based on options - if (options.includeSettings !== false) { + if (options.includeSettings !== false && backupData.data.settings) { await this.restoreSettings(backupData.data.settings); } - if (options.includeLibrary !== false) { + if (options.includeLibrary !== false && backupData.data.library) { await this.restoreLibrary(backupData.data.library); } - if (options.includeWatchProgress !== false) { + if (options.includeWatchProgress !== false && backupData.data.watchProgress) { await this.restoreWatchProgress(backupData.data.watchProgress); } - if (options.includeAddons !== false) { + if (options.includeAddons !== false && backupData.data.addons) { await this.restoreAddons(backupData.data.addons); } - if (options.includeDownloads !== false) { + if (options.includeDownloads !== false && backupData.data.downloads) { await this.restoreDownloads(backupData.data.downloads); } @@ -233,12 +255,48 @@ export class BackupService { await this.restoreLocalScrapers(backupData.data.localScrapers); } + if (options.includeApiKeys !== false && backupData.data.apiKeys) { + await this.restoreApiKeys(backupData.data.apiKeys); + } + + if (options.includeCatalogSettings !== false && backupData.data.catalogSettings) { + await this.restoreCatalogSettings(backupData.data.catalogSettings); + } + + if (options.includeUserPreferences !== false) { + if (backupData.data.addonOrder) { + await this.restoreAddonOrder(backupData.data.addonOrder); + } + if (backupData.data.removedAddons) { + await this.restoreRemovedAddons(backupData.data.removedAddons); + } + if (backupData.data.globalSeasonViewMode) { + await this.restoreGlobalSeasonViewMode(backupData.data.globalSeasonViewMode); + } + if (backupData.data.hasCompletedOnboarding !== undefined) { + await this.restoreHasCompletedOnboarding(backupData.data.hasCompletedOnboarding); + } + if (backupData.data.showLoginHintToastOnce !== undefined) { + await this.restoreShowLoginHintToastOnce(backupData.data.showLoginHintToastOnce); + } + } + // Restore additional data - await this.restoreSubtitleSettings(backupData.data.subtitles); - await this.restoreTombstones(backupData.data.tombstones); - await this.restoreContinueWatchingRemoved(backupData.data.continueWatchingRemoved); - await this.restoreContentDuration(backupData.data.contentDuration); - await this.restoreSyncQueue(backupData.data.syncQueue); + if (backupData.data.subtitles) { + await this.restoreSubtitleSettings(backupData.data.subtitles); + } + if (backupData.data.tombstones) { + await this.restoreTombstones(backupData.data.tombstones); + } + if (backupData.data.continueWatchingRemoved) { + await this.restoreContinueWatchingRemoved(backupData.data.continueWatchingRemoved); + } + if (backupData.data.contentDuration) { + await this.restoreContentDuration(backupData.data.contentDuration); + } + if (backupData.data.syncQueue) { + await this.restoreSyncQueue(backupData.data.syncQueue); + } logger.info('[BackupService] Backup restore completed successfully'); } catch (error) { @@ -405,15 +463,15 @@ export class BackupService { } } - private async getContinueWatchingRemoved(): Promise { + private async getContinueWatchingRemoved(): Promise> { try { const scope = await this.getUserScope(); const scopedKey = `@user:${scope}:@continue_watching_removed`; const removedJson = await AsyncStorage.getItem(scopedKey); - return removedJson ? JSON.parse(removedJson) : []; + return removedJson ? JSON.parse(removedJson) : {}; } catch (error) { logger.error('[BackupService] Failed to get continue watching removed:', error); - return []; + return {}; } } @@ -535,6 +593,93 @@ export class BackupService { } } + private async getApiKeys(): Promise<{ mdblistApiKey?: string; openRouterApiKey?: string }> { + try { + const [mdblistKey, openRouterKey] = await Promise.all([ + AsyncStorage.getItem('mdblist_api_key'), + AsyncStorage.getItem('openrouter_api_key') + ]); + + return { + mdblistApiKey: mdblistKey || undefined, + openRouterApiKey: openRouterKey || undefined + }; + } catch (error) { + logger.error('[BackupService] Failed to get API keys:', error); + return {}; + } + } + + private async getCatalogSettings(): Promise { + try { + const catalogSettingsJson = await AsyncStorage.getItem('catalog_settings'); + return catalogSettingsJson ? JSON.parse(catalogSettingsJson) : null; + } catch (error) { + logger.error('[BackupService] Failed to get catalog settings:', error); + return null; + } + } + + private async getAddonOrder(): Promise { + try { + const scope = await this.getUserScope(); + const scopedKey = `@user:${scope}:stremio-addon-order`; + + // Try scoped key first, then legacy keys + const [scopedOrder, legacyOrder, localOrder] = await Promise.all([ + AsyncStorage.getItem(scopedKey), + AsyncStorage.getItem('stremio-addon-order'), + AsyncStorage.getItem('@user:local:stremio-addon-order') + ]); + + const orderJson = scopedOrder || legacyOrder || localOrder; + return orderJson ? JSON.parse(orderJson) : []; + } catch (error) { + logger.error('[BackupService] Failed to get addon order:', error); + return []; + } + } + + private async getRemovedAddons(): Promise { + try { + const removedAddonsJson = await AsyncStorage.getItem('user_removed_addons'); + return removedAddonsJson ? JSON.parse(removedAddonsJson) : []; + } catch (error) { + logger.error('[BackupService] Failed to get removed addons:', error); + return []; + } + } + + private async getGlobalSeasonViewMode(): Promise { + try { + const mode = await AsyncStorage.getItem('global_season_view_mode'); + return mode || undefined; + } catch (error) { + logger.error('[BackupService] Failed to get global season view mode:', error); + return undefined; + } + } + + private async getHasCompletedOnboarding(): Promise { + try { + const value = await AsyncStorage.getItem('hasCompletedOnboarding'); + return value === 'true' ? true : value === 'false' ? false : undefined; + } catch (error) { + logger.error('[BackupService] Failed to get has completed onboarding:', error); + return undefined; + } + } + + private async getShowLoginHintToastOnce(): Promise { + try { + const value = await AsyncStorage.getItem('showLoginHintToastOnce'); + return value === 'true' ? true : value === 'false' ? false : undefined; + } catch (error) { + logger.error('[BackupService] Failed to get show login hint toast once:', error); + return undefined; + } + } + // Private helper methods for data restoration private async restoreSettings(settings: AppSettings): Promise { try { @@ -610,7 +755,7 @@ export class BackupService { } } - private async restoreContinueWatchingRemoved(removed: string[]): Promise { + private async restoreContinueWatchingRemoved(removed: Record): Promise { try { const scope = await this.getUserScope(); const scopedKey = `@user:${scope}:@continue_watching_removed`; @@ -732,6 +877,87 @@ export class BackupService { } } + private async restoreApiKeys(apiKeys: { mdblistApiKey?: string; openRouterApiKey?: string }): Promise { + try { + const setPromises: Promise[] = []; + + if (apiKeys.mdblistApiKey) { + setPromises.push(AsyncStorage.setItem('mdblist_api_key', apiKeys.mdblistApiKey)); + } + + if (apiKeys.openRouterApiKey) { + setPromises.push(AsyncStorage.setItem('openrouter_api_key', apiKeys.openRouterApiKey)); + } + + await Promise.all(setPromises); + logger.info('[BackupService] API keys restored'); + } catch (error) { + logger.error('[BackupService] Failed to restore API keys:', error); + } + } + + private async restoreCatalogSettings(catalogSettings: any): Promise { + try { + await AsyncStorage.setItem('catalog_settings', JSON.stringify(catalogSettings)); + logger.info('[BackupService] Catalog settings restored'); + } catch (error) { + logger.error('[BackupService] Failed to restore catalog settings:', error); + } + } + + private async restoreAddonOrder(addonOrder: string[]): Promise { + try { + const scope = await this.getUserScope(); + const scopedKey = `@user:${scope}:stremio-addon-order`; + + // Restore to both scoped and legacy keys for compatibility + await Promise.all([ + AsyncStorage.setItem(scopedKey, JSON.stringify(addonOrder)), + AsyncStorage.setItem('stremio-addon-order', JSON.stringify(addonOrder)) + ]); + + logger.info('[BackupService] Addon order restored'); + } catch (error) { + logger.error('[BackupService] Failed to restore addon order:', error); + } + } + + private async restoreRemovedAddons(removedAddons: string[]): Promise { + try { + await AsyncStorage.setItem('user_removed_addons', JSON.stringify(removedAddons)); + logger.info('[BackupService] Removed addons restored'); + } catch (error) { + logger.error('[BackupService] Failed to restore removed addons:', error); + } + } + + private async restoreGlobalSeasonViewMode(mode: string): Promise { + try { + await AsyncStorage.setItem('global_season_view_mode', mode); + logger.info('[BackupService] Global season view mode restored'); + } catch (error) { + logger.error('[BackupService] Failed to restore global season view mode:', error); + } + } + + private async restoreHasCompletedOnboarding(value: boolean): Promise { + try { + await AsyncStorage.setItem('hasCompletedOnboarding', value.toString()); + logger.info('[BackupService] Has completed onboarding restored'); + } catch (error) { + logger.error('[BackupService] Failed to restore has completed onboarding:', error); + } + } + + private async restoreShowLoginHintToastOnce(value: boolean): Promise { + try { + await AsyncStorage.setItem('showLoginHintToastOnce', value.toString()); + logger.info('[BackupService] Show login hint toast once restored'); + } catch (error) { + logger.error('[BackupService] Failed to restore show login hint toast once:', error); + } + } + private validateBackupData(backupData: any): void { if (!backupData.version || !backupData.timestamp || !backupData.data) { throw new Error('Invalid backup file format'); diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index 572d1cb..8c7b25c 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -87,6 +87,12 @@ export interface StreamingContent { slug?: string; releaseInfo?: string; traktSource?: 'watchlist' | 'continue-watching' | 'watched'; + addonCast?: Array<{ + id: number; + name: string; + character: string; + profile_path: string | null; + }>; } export interface CatalogContent { @@ -737,7 +743,25 @@ class CatalogService { behaviorHints: (meta as any).behaviorHints || undefined, }; - // Cast is handled separately by the dedicated CastSection component via TMDB + // Extract addon cast data if available + // Check for both app_extras.cast (structured) and cast (simple array) formats + if ((meta as any).app_extras?.cast && Array.isArray((meta as any).app_extras.cast)) { + // Structured format with name, character, photo + converted.addonCast = (meta as any).app_extras.cast.map((castMember: any, index: number) => ({ + id: index + 1, // Use index as numeric ID + name: castMember.name || 'Unknown', + character: castMember.character || '', + profile_path: castMember.photo || null + })); + } else if (meta.cast && Array.isArray(meta.cast)) { + // Simple array format with just names + converted.addonCast = meta.cast.map((castName: string, index: number) => ({ + id: index + 1, // Use index as numeric ID + name: castName || 'Unknown', + character: '', // No character info available in simple format + profile_path: null // No profile images available in simple format + })); + } // Log if rich metadata is found if ((meta as any).trailerStreams?.length > 0) { @@ -748,6 +772,10 @@ class CatalogService { logger.log(`πŸ”— Enhanced metadata: Found ${(meta as any).links.length} links for ${meta.name}`); } + if (converted.addonCast && converted.addonCast.length > 0) { + logger.log(`🎭 Enhanced metadata: Found ${converted.addonCast.length} cast members from addon for ${meta.name}`); + } + // Handle videos/episodes if available if ((meta as any).videos) { converted.videos = (meta as any).videos;