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;