From f96d1f1af38d4702adfbd00ab489bd9cd7358c21 Mon Sep 17 00:00:00 2001 From: tapframe Date: Thu, 1 May 2025 20:26:43 +0530 Subject: [PATCH] Update app configuration and improve stream handling; add local network and microphone usage descriptions in app.json, enhance stream loading logic in useMetadata by removing external source requests, and implement error handling in VideoPlayer. Update StreamsScreen to filter out deprecated sources for better clarity. --- app.json | 21 +- package.json | 3 +- patch-package.js | 42 ++++ src/hooks/useMetadata.ts | 221 ++------------------ src/patches/react-native-video+6.12.0.patch | 44 ++++ src/screens/StreamsScreen.tsx | 8 +- src/screens/VideoPlayer.tsx | 7 + src/services/stremioService.ts | 43 +++- 8 files changed, 161 insertions(+), 228 deletions(-) create mode 100644 patch-package.js create mode 100644 src/patches/react-native-video+6.12.0.patch diff --git a/app.json b/app.json index b611bcb08..07c328dc0 100644 --- a/app.json +++ b/app.json @@ -18,9 +18,26 @@ "infoPlist": { "NSAppTransportSecurity": { "NSAllowsArbitraryLoads": true - } + }, + "NSBonjourServices": [ + "_http._tcp" + ], + "NSLocalNetworkUsageDescription": "App uses the local network to discover and connect to devices.", + "NSMicrophoneUsageDescription": "This app does not require microphone access.", + "UIBackgroundModes": ["audio"], + "LSSupportsOpeningDocumentsInPlace": true, + "UIFileSharingEnabled": true }, - "bundleIdentifier": "com.nuvio.app" + "bundleIdentifier": "com.nuvio.app", + "associatedDomains": [], + "documentTypes": [ + { + "name": "Matroska Video", + "role": "viewer", + "utis": ["org.matroska.mkv"], + "extensions": ["mkv"] + } + ] }, "android": { "adaptiveIcon": { diff --git a/package.json b/package.json index a18105d21..d78bd87d6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "start": "expo start", "android": "expo run:android", "ios": "expo run:ios", - "web": "expo start --web" + "web": "expo start --web", + "postinstall": "node patch-package.js" }, "dependencies": { "@expo/metro-runtime": "~4.0.1", diff --git a/patch-package.js b/patch-package.js new file mode 100644 index 000000000..6998999b0 --- /dev/null +++ b/patch-package.js @@ -0,0 +1,42 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// Directory containing patches +const patchesDir = path.join(__dirname, 'src/patches'); + +// Check if the directory exists +if (!fs.existsSync(patchesDir)) { + console.error(`Patches directory not found: ${patchesDir}`); + process.exit(1); +} + +// Get all patch files +const patches = fs.readdirSync(patchesDir).filter(file => file.endsWith('.patch')); + +if (patches.length === 0) { + console.log('No patch files found.'); + process.exit(0); +} + +console.log(`Found ${patches.length} patch files.`); + +// Apply each patch +patches.forEach(patchFile => { + const patchPath = path.join(patchesDir, patchFile); + console.log(`Applying patch: ${patchFile}`); + + try { + // Use the patch command to apply the patch file + execSync(`patch -p1 < ${patchPath}`, { + stdio: 'inherit', + cwd: process.cwd() + }); + console.log(`āœ… Successfully applied patch: ${patchFile}`); + } catch (error) { + console.error(`āŒ Failed to apply patch ${patchFile}:`, error.message); + // Continue with other patches even if one fails + } +}); + +console.log('Patch process completed.'); \ No newline at end of file diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 6bc3db7bf..e3074f590 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -577,9 +577,12 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = const loadStreams = async () => { const startTime = Date.now(); try { - console.log('šŸš€ [loadStreams] START - Loading movie streams for:', id); + console.log('šŸš€ [loadStreams] START - Loading streams for:', id); updateLoadingState(); + // Always clear streams first to ensure we don't show stale data + setGroupedStreams({}); + // Get TMDB ID for external sources first before starting parallel requests console.log('šŸ” [loadStreams] Getting TMDB ID for:', id); let tmdbId; @@ -598,70 +601,18 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = console.log('šŸ”„ [loadStreams] Starting stream requests'); - const fetchPromises = []; - - // Start Stremio request using the new callback method - // We don't push this promise anymore, as results are handled by callback + // Start Stremio request using the callback method processStremioSource(type, id, false); - // Start Source 1 request if we have a TMDB ID - if (tmdbId) { - const source1Promise = processExternalSource('source1', (async () => { - try { - const streams = await fetchExternalStreams( - `https://nice-month-production.up.railway.app/embedsu/${tmdbId}`, - 'Source 1' - ); - - if (streams.length > 0) { - return { - 'source_1': { - addonName: 'Source 1', - streams - } - }; - } - return {}; - } catch (error) { - console.error('āŒ [loadStreams:source1] Error fetching Source 1 streams:', error); - return {}; - } - })(), false); - fetchPromises.push(source1Promise); - } + // No external sources are used anymore + const fetchPromises: Promise[] = []; - // Start Source 2 request if we have a TMDB ID - if (tmdbId) { - const source2Promise = processExternalSource('source2', (async () => { - try { - const streams = await fetchExternalStreams( - `https://vidsrc-api-js-phz6.onrender.com/embedsu/${tmdbId}`, - 'Source 2' - ); - - if (streams.length > 0) { - return { - 'source_2': { - addonName: 'Source 2', - streams - } - }; - } - return {}; - } catch (error) { - console.error('āŒ [loadStreams:source2] Error fetching Source 2 streams:', error); - return {}; - } - })(), false); - fetchPromises.push(source2Promise); - } - - // Wait only for external promises now + // Wait only for external promises now (none in this case) const results = await Promise.allSettled(fetchPromises); const totalTime = Date.now() - startTime; console.log(`āœ… [loadStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`); - const sourceTypes = ['source1', 'source2']; // Removed 'stremio' + const sourceTypes: string[] = []; // No external sources results.forEach((result, index) => { const source = sourceTypes[Math.min(index, sourceTypes.length - 1)]; console.log(`šŸ“Š [loadStreams:${source}] Status: ${result.status}`); @@ -729,72 +680,19 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = console.log('šŸ”„ [loadEpisodeStreams] Starting stream requests'); - const fetchPromises = []; + const fetchPromises: Promise[] = []; - // Start Stremio request using the new callback method - // We don't push this promise anymore + // Start Stremio request using the callback method processStremioSource('series', episodeId, true); - // Start Source 1 request if we have a TMDB ID - if (tmdbId) { - const source1Promise = processExternalSource('source1', (async () => { - try { - const streams = await fetchExternalStreams( - `https://nice-month-production.up.railway.app/embedsu/${tmdbId}${episodeQuery}`, - 'Source 1', - true - ); - - if (streams.length > 0) { - return { - 'source_1': { - addonName: 'Source 1', - streams - } - }; - } - return {}; - } catch (error) { - console.error('āŒ [loadEpisodeStreams:source1] Error fetching Source 1 streams:', error); - return {}; - } - })(), true); - fetchPromises.push(source1Promise); - } + // No external sources are used anymore - // Start Source 2 request if we have a TMDB ID - if (tmdbId) { - const source2Promise = processExternalSource('source2', (async () => { - try { - const streams = await fetchExternalStreams( - `https://vidsrc-api-js-phz6.onrender.com/embedsu/${tmdbId}${episodeQuery}`, - 'Source 2', - true - ); - - if (streams.length > 0) { - return { - 'source_2': { - addonName: 'Source 2', - streams - } - }; - } - return {}; - } catch (error) { - console.error('āŒ [loadEpisodeStreams:source2] Error fetching Source 2 streams:', error); - return {}; - } - })(), true); - fetchPromises.push(source2Promise); - } - - // Wait only for external promises now + // Wait only for external promises now (none in this case) const results = await Promise.allSettled(fetchPromises); const totalTime = Date.now() - startTime; console.log(`āœ… [loadEpisodeStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`); - const sourceTypes = ['source1', 'source2']; // Removed 'stremio' + const sourceTypes: string[] = []; // No external sources results.forEach((result, index) => { const source = sourceTypes[Math.min(index, sourceTypes.length - 1)]; console.log(`šŸ“Š [loadEpisodeStreams:${source}] Status: ${result.status}`); @@ -834,97 +732,6 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = } }; - const fetchExternalStreams = async (url: string, sourceName: string, isEpisode = false) => { - try { - console.log(`\n🌐 [${sourceName}] Starting fetch request...`); - console.log(`šŸ“ URL: ${url}`); - - // Add proper headers to ensure we get JSON response - const headers = { - 'Accept': 'application/json', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' - }; - console.log('šŸ“‹ Request Headers:', headers); - - // Make the fetch request - console.log(`ā³ [${sourceName}] Making fetch request...`); - const response = await fetch(url, { headers }); - console.log(`āœ… [${sourceName}] Response received`); - console.log(`šŸ“Š Status: ${response.status} ${response.statusText}`); - console.log(`šŸ”¤ Content-Type:`, response.headers.get('content-type')); - - // Check if response is ok - if (!response.ok) { - console.error(`āŒ [${sourceName}] HTTP error: ${response.status}`); - console.error(`šŸ“ Status Text: ${response.statusText}`); - throw new Error(`HTTP error! status: ${response.status}`); - } - - // Try to parse JSON - console.log(`šŸ“‘ [${sourceName}] Reading response body...`); - const text = await response.text(); - console.log(`šŸ“„ [${sourceName}] Response body (first 300 chars):`, text.substring(0, 300)); - - let data; - try { - console.log(`šŸ”„ [${sourceName}] Parsing JSON...`); - data = JSON.parse(text); - console.log(`āœ… [${sourceName}] JSON parsed successfully`); - } catch (e) { - console.error(`āŒ [${sourceName}] JSON parse error:`, e); - console.error(`šŸ“ [${sourceName}] Raw response:`, text.substring(0, 200)); - throw new Error('Invalid JSON response'); - } - - // Transform the response - console.log(`šŸ”„ [${sourceName}] Processing sources...`); - if (data && data.sources && Array.isArray(data.sources)) { - console.log(`šŸ“¦ [${sourceName}] Found ${data.sources.length} source(s)`); - - const transformedStreams = []; - for (const source of data.sources) { - console.log(`\nšŸ“‚ [${sourceName}] Processing source:`, source); - - if (source.files && Array.isArray(source.files)) { - console.log(`šŸ“ [${sourceName}] Found ${source.files.length} file(s) in source`); - - for (const file of source.files) { - console.log(`šŸŽ„ [${sourceName}] Processing file:`, file); - const stream = { - url: file.file, - title: `${sourceName} - ${file.quality || 'Unknown'}`, - name: `${sourceName} - ${file.quality || 'Unknown'}`, - behaviorHints: { - notWebReady: false, - headers: source.headers || {} - } - }; - console.log(`✨ [${sourceName}] Created stream:`, stream); - transformedStreams.push(stream); - } - } else { - console.log(`āš ļø [${sourceName}] No files array found in source or invalid format`); - } - } - - console.log(`\nšŸŽ‰ [${sourceName}] Successfully processed ${transformedStreams.length} stream(s)`); - return transformedStreams; - } - - console.log(`āš ļø [${sourceName}] No valid sources found in response`); - return []; - } catch (error) { - console.error(`\nāŒ [${sourceName}] Error fetching streams:`, error); - console.error(`šŸ“ URL: ${url}`); - if (error instanceof Error) { - console.error(`šŸ’„ Error name: ${error.name}`); - console.error(`šŸ’„ Error message: ${error.message}`); - console.error(`šŸ’„ Stack trace: ${error.stack}`); - } - return []; - } - }; - const handleSeasonChange = useCallback((seasonNumber: number) => { if (selectedSeason === seasonNumber) return; diff --git a/src/patches/react-native-video+6.12.0.patch b/src/patches/react-native-video+6.12.0.patch new file mode 100644 index 000000000..2ac9ccc3f --- /dev/null +++ b/src/patches/react-native-video+6.12.0.patch @@ -0,0 +1,44 @@ +diff --git a/node_modules/react-native-video/ios/Video/RCTVideo.m b/node_modules/react-native-video/ios/Video/RCTVideo.m +index 79d88de..a28a21e 100644 +--- a/node_modules/react-native-video/ios/Video/RCTVideo.m ++++ b/node_modules/react-native-video/ios/Video/RCTVideo.m +@@ -1023,7 +1023,9 @@ static NSString *const statusKeyPath = @"status"; + + /* The player used to render the video */ + AVPlayer *_player; +- AVPlayerLayer *_playerLayer; ++ // Use strong reference instead of weak to prevent deallocation issues ++ __strong AVPlayerLayer *_playerLayer; ++ + NSURL *_videoURL; + + /* IOS < 10 seek optimization */ +@@ -1084,7 +1086,16 @@ - (void)removeFromSuperview + + _player = nil; + _playerItem = nil; +- _playerLayer = nil; ++ ++ // Properly clean up the player layer ++ if (_playerLayer) { ++ [_playerLayer removeFromSuperlayer]; ++ // Set animation keys to nil before releasing to avoid crashes ++ [_playerLayer removeAllAnimations]; ++ _playerLayer = nil; ++ } ++ ++ [[NSNotificationCenter defaultCenter] removeObserver:self]; + } + + #pragma mark - App lifecycle handlers +@@ -1116,7 +1127,8 @@ - (void)applicationDidEnterBackground:(NSNotification *)notification + + - (void)applicationWillEnterForeground:(NSNotification *)notification + { +- if (_playInBackground || _playWhenInactive || _paused) return; ++ // Resume playback even if originally playing in background ++ if (_paused) return; + + [_player play]; + [_player setRate:_rate]; + } \ No newline at end of file diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index faf828434..8881b2a2b 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -542,14 +542,12 @@ export const StreamsScreen = () => { if (indexB !== -1) return 1; return 0; }) - .filter(provider => provider !== 'source_1' && provider !== 'source_2') // Filter out source_1 and source_2 .map(provider => { const addonInfo = streams[provider]; const installedAddon = installedAddons.find(addon => addon.id === provider); let displayName = provider; - if (provider === 'external_sources') displayName = 'External Sources'; - else if (installedAddon) displayName = installedAddon.name; + if (installedAddon) displayName = installedAddon.name; else if (addonInfo?.addonName) displayName = addonInfo.addonName; return { id: provider, name: displayName }; @@ -562,10 +560,6 @@ export const StreamsScreen = () => { const installedAddons = stremioService.getInstalledAddons(); return Object.entries(streams) - .filter(([addonId]) => { - // Filter out source_1 and source_2 - return addonId !== 'source_1' && addonId !== 'source_2'; - }) .sort(([addonIdA], [addonIdB]) => { const indexA = installedAddons.findIndex(addon => addon.id === addonIdA); const indexB = installedAddons.findIndex(addon => addon.id === addonIdB); diff --git a/src/screens/VideoPlayer.tsx b/src/screens/VideoPlayer.tsx index 596e171eb..7aa4b474e 100644 --- a/src/screens/VideoPlayer.tsx +++ b/src/screens/VideoPlayer.tsx @@ -633,6 +633,12 @@ const VideoPlayer: React.FC = () => { `); }; + // Add onError handler + const handleError = (error: any) => { + logger.error('[VideoPlayer] Playback Error:', error); + // Optionally, you could show an error message to the user here + }; + return ( { onTextTracks={onTextTracks} onBuffer={onBuffer} onLoadStart={onLoadStart} + onError={handleError} /> {/* Slider Container with buffer indicator */} diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index bad89ff29..da2daf230 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -558,21 +558,42 @@ class StremioService { } // Log the detailed resources structure for debugging - // logger.log(`šŸ“‹ [getStreams] Checking addon ${addon.id} resources:`, JSON.stringify(addon.resources)); // Verbose, uncomment if needed + logger.log(`šŸ“‹ [getStreams] Checking addon ${addon.id} resources:`, JSON.stringify(addon.resources)); - // Check if the addon has a stream resource for this type - const hasStreamResource = addon.resources.some( - resource => { - const result = resource.name === 'stream' && resource.types && resource.types.includes(type); - // logger.log(`šŸ”Ž [getStreams] Addon ${addon.id} resource ${resource.name}: supports ${type}? ${result}`); // Verbose - return result; - } - ); + let hasStreamResource = false; + + // Handle both resource formats: + // 1. Standard format: array of ResourceObjects with name and types properties + // 2. Simple format: string array ["stream"] with separate types array in the addon + + // Check for standard format (array of objects) + if (addon.resources.length > 0 && typeof addon.resources[0] === 'object') { + // Type-safe check for standard format (objects with name/types) + hasStreamResource = addon.resources.some( + resource => { + if (typeof resource === 'object' && resource !== null) { + const typedResource = resource as ResourceObject; + return typedResource.name === 'stream' && + Array.isArray(typedResource.types) && + typedResource.types.includes(type); + } + return false; + } + ); + } + // Check for simple format (string array) + else if (Array.isArray(addon.resources) && addon.types) { + // Check if resources array contains 'stream' and types array includes the desired type + hasStreamResource = + (addon.resources as unknown as string[]).includes('stream') && + Array.isArray(addon.types) && + addon.types.includes(type); + } if (!hasStreamResource) { - // logger.log(`āŒ [getStreams] Addon ${addon.id} does not support streaming ${type}`); // Verbose + logger.log(`āŒ [getStreams] Addon ${addon.id} does not support streaming ${type}`); } else { - // logger.log(`āœ… [getStreams] Addon ${addon.id} supports streaming ${type}`); // Verbose + logger.log(`āœ… [getStreams] Addon ${addon.id} supports streaming ${type}`); } return hasStreamResource;