diff --git a/local-scrapers-repo b/local-scrapers-repo index 63d560d..22ed3a1 160000 --- a/local-scrapers-repo +++ b/local-scrapers-repo @@ -1 +1 @@ -Subproject commit 63d560d55f1a84a16318525ad4eb1db5162e059c +Subproject commit 22ed3a1c96ed2a8adf5bf9f277acd9d8c53c069c diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index e92ab4d..4d034d2 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -39,7 +39,7 @@ import LogoSourceSettings from '../screens/LogoSourceSettings'; import ThemeScreen from '../screens/ThemeScreen'; import ProfilesScreen from '../screens/ProfilesScreen'; import OnboardingScreen from '../screens/OnboardingScreen'; -import ScraperSettingsScreen from '../screens/ScraperSettingsScreen'; +import PluginsScreen from '../screens/PluginsScreen'; // Stack navigator types export type RootStackParamList = { @@ -1028,7 +1028,7 @@ const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStack /> StyleSheet.create({ opacity: 0.5, }, disabledImage: { - opacity: 0.3, - }, - }); + opacity: 0.3, + }, + availableIndicator: { + backgroundColor: colors.primary, + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + marginLeft: 8, + }, + availableIndicatorText: { + color: colors.white, + fontSize: 10, + fontWeight: '600', + }, +}); -const ScraperSettingsScreen: React.FC = () => { +const PluginsScreen: React.FC = () => { const navigation = useNavigation(); const { settings, updateSetting } = useSettings(); const { currentTheme } = useTheme(); @@ -337,7 +349,7 @@ const ScraperSettingsScreen: React.FC = () => { const loadScrapers = async () => { try { - const scrapers = await localScraperService.getInstalledScrapers(); + const scrapers = await localScraperService.getAvailableScrapers(); setInstalledScrapers(scrapers); } catch (error) { logger.error('[ScraperSettings] Failed to load scrapers:', error); @@ -395,7 +407,7 @@ const ScraperSettingsScreen: React.FC = () => { try { setIsRefreshing(true); await localScraperService.refreshRepository(); - await loadScrapers(); + await loadScrapers(); // This will now load available scrapers from manifest Alert.alert('Success', 'Repository refreshed successfully'); } catch (error) { logger.error('[ScraperSettings] Failed to refresh repository:', error); @@ -411,11 +423,25 @@ const ScraperSettingsScreen: React.FC = () => { const handleToggleScraper = async (scraperId: string, enabled: boolean) => { try { + if (enabled) { + // If enabling a scraper, ensure it's installed first + const installedScrapers = await localScraperService.getInstalledScrapers(); + const isInstalled = installedScrapers.some(scraper => scraper.id === scraperId); + + if (!isInstalled) { + // Need to install the scraper first + setIsRefreshing(true); + await localScraperService.refreshRepository(); + setIsRefreshing(false); + } + } + await localScraperService.setScraperEnabled(scraperId, enabled); await loadScrapers(); } catch (error) { logger.error('[ScraperSettings] Failed to toggle scraper:', error); Alert.alert('Error', 'Failed to update scraper status'); + setIsRefreshing(false); } }; @@ -502,7 +528,7 @@ const ScraperSettingsScreen: React.FC = () => { - Local Scrapers + Plugins { - {/* Installed Scrapers */} + {/* Available Scrapers */} - Installed Scrapers + Available Scrapers {installedScrapers.length > 0 && settings.enableLocalScrapers && ( { )} + + Scrapers available in the repository. Only enabled scrapers that are also installed will be used for streaming. + {installedScrapers.length === 0 ? ( - No Scrapers Installed + No Scrapers Available - Configure a repository above to install scrapers. + Configure a repository above to view available scrapers. ) : ( - {installedScrapers.map((scraper) => ( - - {scraper.logo ? ( - - ) : ( - - )} - - {scraper.name} - {scraper.description} - - v{scraper.version} - - - {scraper.supportedTypes && Array.isArray(scraper.supportedTypes) ? scraper.supportedTypes.join(', ') : 'Unknown'} - - {scraper.contentLanguage && Array.isArray(scraper.contentLanguage) && scraper.contentLanguage.length > 0 && ( - <> - - - {scraper.contentLanguage.map(lang => lang.toUpperCase()).join(', ')} - - - )} + {installedScrapers.map((scraper) => { + // Check if scraper is actually installed (has cached code) + const isInstalled = localScraperService.getInstalledScrapers().then(installed => + installed.some(s => s.id === scraper.id) + ); + + return ( + + {scraper.logo ? ( + + ) : ( + + )} + + + {scraper.name} + {scraper.manifestEnabled === false ? ( + + Disabled + + ) : !scraper.enabled && ( + + Available + + )} + + {scraper.description} + + v{scraper.version} + + + {scraper.supportedTypes && Array.isArray(scraper.supportedTypes) ? scraper.supportedTypes.join(', ') : 'Unknown'} + + {scraper.contentLanguage && Array.isArray(scraper.contentLanguage) && scraper.contentLanguage.length > 0 && ( + <> + + + {scraper.contentLanguage.map(lang => lang.toUpperCase()).join(', ')} + + + )} + + handleToggleScraper(scraper.id, enabled)} + trackColor={{ false: colors.elevation3, true: colors.primary }} + thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'} + disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false} + style={{ opacity: (!settings.enableLocalScrapers || scraper.manifestEnabled === false) ? 0.5 : 1 }} + /> - handleToggleScraper(scraper.id, enabled)} - trackColor={{ false: colors.elevation3, true: colors.primary }} - thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'} - disabled={!settings.enableLocalScrapers} - /> - - ))} + ); + })} )} @@ -945,4 +993,4 @@ const styles = StyleSheet.create({ }, }); -export default ScraperSettingsScreen; \ No newline at end of file +export default PluginsScreen; \ No newline at end of file diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 0844eac..91cd907 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -323,6 +323,13 @@ const SettingsScreen: React.FC = () => { renderControl={ChevronRight} onPress={() => navigation.navigate('Addons')} /> + navigation.navigate('ScraperSettings')} + /> { renderControl={ChevronRight} onPress={() => navigation.navigate('PlayerSettings')} /> - navigation.navigate('ScraperSettings')} - /> { + logger.log(`[LocalScraperService] getAvailableScrapers - Scraper ${scraper.name}: enabled=${scraper.enabled}`); + }); logger.log('[LocalScraperService] Found', manifest.scrapers.length, 'scrapers in repository'); - // Download and install each scraper + // Get current manifest scraper IDs + const manifestScraperIds = new Set(manifest.scrapers.map(s => s.id)); + + // Remove scrapers that are no longer in the manifest + const currentScraperIds = Array.from(this.installedScrapers.keys()); + for (const scraperId of currentScraperIds) { + if (!manifestScraperIds.has(scraperId)) { + logger.log('[LocalScraperService] Removing scraper no longer in manifest:', this.installedScrapers.get(scraperId)?.name || scraperId); + this.installedScrapers.delete(scraperId); + this.scraperCode.delete(scraperId); + // Remove from AsyncStorage cache + await AsyncStorage.removeItem(`scraper-code-${scraperId}`); + } + } + + // Download and install each scraper from manifest for (const scraperInfo of manifest.scrapers) { await this.downloadScraper(scraperInfo); } @@ -296,6 +328,65 @@ class LocalScraperService { return Array.from(this.installedScrapers.values()); } + // Get available scrapers from manifest.json (for display in settings) + async getAvailableScrapers(): Promise { + if (!this.repositoryUrl) { + logger.log('[LocalScraperService] No repository URL configured, returning installed scrapers'); + return this.getInstalledScrapers(); + } + + try { + logger.log('[LocalScraperService] Fetching available scrapers from manifest'); + + // Fetch manifest with cache busting + const baseManifestUrl = this.repositoryUrl.endsWith('/') + ? `${this.repositoryUrl}manifest.json` + : `${this.repositoryUrl}/manifest.json`; + const manifestUrl = `${baseManifestUrl}?t=${Date.now()}`; + + const response = await axios.get(manifestUrl, { + timeout: 10000, + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }); + const manifest: ScraperManifest = response.data; + + // Return scrapers from manifest, respecting manifest's enabled field + const availableScrapers = manifest.scrapers.map(scraperInfo => { + const installedScraper = this.installedScrapers.get(scraperInfo.id); + + // Create a copy with manifest data + const scraperWithManifestData = { + ...scraperInfo, + // Store the manifest's enabled state separately + manifestEnabled: scraperInfo.enabled, + // If manifest says enabled: false, scraper cannot be enabled + // If manifest says enabled: true, use installed state or default to false + enabled: scraperInfo.enabled ? (installedScraper?.enabled ?? false) : false + }; + + return scraperWithManifestData; + }); + + logger.log('[LocalScraperService] Found', availableScrapers.length, 'available scrapers in repository'); + + // Log final scraper states being returned to UI + availableScrapers.forEach(scraper => { + logger.log(`[LocalScraperService] Final scraper ${scraper.name}: manifestEnabled=${scraper.manifestEnabled}, enabled=${scraper.enabled}`); + }); + + return availableScrapers; + + } catch (error) { + logger.error('[LocalScraperService] Failed to fetch available scrapers from manifest:', error); + // Fallback to installed scrapers if manifest fetch fails + return this.getInstalledScrapers(); + } + } + // Enable/disable scraper async setScraperEnabled(scraperId: string, enabled: boolean): Promise { await this.ensureInitialized(); @@ -313,8 +404,14 @@ class LocalScraperService { async getStreams(type: string, tmdbId: string, season?: number, episode?: number, callback?: ScraperCallback): Promise { await this.ensureInitialized(); - const enabledScrapers = Array.from(this.installedScrapers.values()) - .filter(scraper => scraper.enabled && scraper.supportedTypes.includes(type as 'movie' | 'tv')); + // Get available scrapers from manifest (respects manifestEnabled) + const availableScrapers = await this.getAvailableScrapers(); + const enabledScrapers = availableScrapers + .filter(scraper => + scraper.enabled && + scraper.manifestEnabled !== false && + scraper.supportedTypes.includes(type as 'movie' | 'tv') + ); if (enabledScrapers.length === 0) { logger.log('[LocalScraperService] No enabled scrapers found for type:', type);