mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
added langauge filtering for plugnins
This commit is contained in:
parent
0c14d8641d
commit
fb316d9f37
10 changed files with 477 additions and 31 deletions
9
App.tsx
9
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -858,6 +858,7 @@ const MetadataScreen: React.FC = () => {
|
|||
cast={cast}
|
||||
loadingCast={loadingCast}
|
||||
onSelectCastMember={handleSelectCastMember}
|
||||
isTmdbEnrichmentEnabled={settings.enrichMetadataWithTMDB}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue