added langauge filtering for plugnins

This commit is contained in:
tapframe 2025-10-11 01:06:37 +05:30
parent 0c14d8641d
commit fb316d9f37
10 changed files with 477 additions and 31 deletions

View file

@ -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);

View file

@ -17,12 +17,14 @@ interface CastSectionProps {
cast: any[];
loadingCast: boolean;
onSelectCastMember: (castMember: any) => void;
isTmdbEnrichmentEnabled?: boolean;
}
export const CastSection: React.FC<CastSectionProps> = ({
cast,
loadingCast,
onSelectCastMember,
isTmdbEnrichmentEnabled = true,
}) => {
const { currentTheme } = useTheme();
@ -80,7 +82,7 @@ export const CastSection: React.FC<CastSectionProps> = ({
)}
</View>
<Text style={[styles.castName, { color: currentTheme.colors.text }]} numberOfLines={1}>{item.name}</Text>
{item.character && (
{isTmdbEnrichmentEnabled && item.character && (
<Text style={[styles.characterName, { color: currentTheme.colors.textMuted }]} numberOfLines={1}>{item.character}</Text>
)}
</TouchableOpacity>

View file

@ -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([]);

View file

@ -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 () => {

View file

@ -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

View file

@ -858,6 +858,7 @@ const MetadataScreen: React.FC = () => {
cast={cast}
loadingCast={loadingCast}
onSelectCastMember={handleSelectCastMember}
isTmdbEnrichmentEnabled={settings.enrichMetadataWithTMDB}
/>
)}

View file

@ -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 = () => {
)}
</CollapsibleSection>
{/* Language Filtering */}
<CollapsibleSection
title="Language Filtering"
isExpanded={expandedSections.quality}
onToggle={() => toggleSection('quality')}
colors={colors}
styles={styles}
>
<Text style={styles.sectionDescription}>
Exclude specific languages from search results. Tap on a language to exclude it from plugin results.
</Text>
<Text style={[styles.infoText, { marginTop: 8, fontSize: 13, color: colors.mediumEmphasis }]}>
<Text style={{ fontWeight: '600' }}>Note:</Text> This filter only applies to providers that include language information in their stream names. It does not affect other providers.
</Text>
<View style={styles.qualityChipsContainer}>
{languageOptions.map((language) => {
const isExcluded = (settings.excludedLanguages || []).includes(language);
return (
<TouchableOpacity
key={language}
style={[
styles.qualityChip,
isExcluded && styles.qualityChipSelected,
!settings.enableLocalScrapers && styles.disabledButton
]}
onPress={() => handleToggleLanguageExclusion(language)}
disabled={!settings.enableLocalScrapers}
>
<Text style={[
styles.qualityChipText,
isExcluded && styles.qualityChipTextSelected,
!settings.enableLocalScrapers && styles.disabledText
]}>
{isExcluded ? '✕ ' : ''}{language}
</Text>
</TouchableOpacity>
);
})}
</View>
{(settings.excludedLanguages || []).length > 0 && (
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
Excluded languages: {(settings.excludedLanguages || []).join(', ')}
</Text>
)}
</CollapsibleSection>
{/* About */}
<View style={[styles.section, styles.lastSection]}>
<Text style={styles.sectionTitle}>About Plugins</Text>

View file

@ -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}`;
}}

View file

@ -20,7 +20,7 @@ export interface BackupData {
downloads: DownloadItem[];
subtitles: any;
tombstones: Record<string, number>;
continueWatchingRemoved: string[];
continueWatchingRemoved: Record<string, number>;
contentDuration: Record<string, number>;
syncQueue: any[];
traktSettings?: any;
@ -32,7 +32,19 @@ export interface BackupData {
scraperSettings: any;
scraperCode: Record<string, string>;
};
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<string[]> {
private async getContinueWatchingRemoved(): Promise<Record<string, number>> {
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<any> {
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<string[]> {
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<string[]> {
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<string | undefined> {
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<boolean | undefined> {
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<boolean | undefined> {
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<void> {
try {
@ -610,7 +755,7 @@ export class BackupService {
}
}
private async restoreContinueWatchingRemoved(removed: string[]): Promise<void> {
private async restoreContinueWatchingRemoved(removed: Record<string, number>): Promise<void> {
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<void> {
try {
const setPromises: Promise<void>[] = [];
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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');

View file

@ -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;