diff --git a/assets/app-icon.png b/assets/app-icon.png index 0692e31..dac4426 100644 Binary files a/assets/app-icon.png and b/assets/app-icon.png differ diff --git a/assets/icon.png b/assets/icon.png index 0692e31..dac4426 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/images/app-icon.png b/assets/images/app-icon.png new file mode 100644 index 0000000..dac4426 Binary files /dev/null and b/assets/images/app-icon.png differ diff --git a/assets/images/icon.png b/assets/images/icon.png index 0692e31..dac4426 100644 Binary files a/assets/images/icon.png and b/assets/images/icon.png differ diff --git a/package-lock.json b/package-lock.json index 36834d6..1103ae9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "nuvio", "version": "1.0.0", "dependencies": { + "@expo/metro-runtime": "~4.0.1", "@expo/vector-icons": "^14.1.0", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/slider": "^4.5.6", @@ -23,6 +24,7 @@ "expo": "~52.0.43", "expo-haptics": "~14.0.1", "expo-image": "~2.0.7", + "expo-intent-launcher": "~12.0.2", "expo-linear-gradient": "~14.0.2", "expo-notifications": "~0.29.14", "expo-screen-orientation": "~8.0.4", @@ -38,7 +40,8 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-svg": "^15.8.0", - "react-native-video": "^6.12.0" + "react-native-video": "^6.12.0", + "react-native-web": "~0.19.13" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -2650,6 +2653,15 @@ "node": ">= 10.0.0" } }, + "node_modules/@expo/metro-runtime": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-4.0.1.tgz", + "integrity": "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw==", + "license": "MIT", + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/@expo/osascript": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.1.6.tgz", @@ -5790,6 +5802,15 @@ "node": ">=8" } }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -6617,6 +6638,15 @@ } } }, + "node_modules/expo-intent-launcher": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/expo-intent-launcher/-/expo-intent-launcher-12.0.2.tgz", + "integrity": "sha512-9JYyuuONE9AxoZgRyztcgfAaIIGvrUQAbFnYOPZcrHSBWhbF1203S3ho1Hk67jWhC72Um8+pJ+rXBxAp3ZZlxA==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-keep-awake": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz", @@ -6802,6 +6832,12 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/fast-loops": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.4.tgz", + "integrity": "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==", + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -7430,6 +7466,12 @@ "node": ">=10.17.0" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7537,6 +7579,16 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/inline-style-prefixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz", + "integrity": "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==", + "license": "MIT", + "dependencies": { + "css-in-js-utils": "^3.1.0", + "fast-loops": "^1.1.3" + } + }, "node_modules/internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -10119,6 +10171,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -10361,6 +10419,30 @@ } } }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/react-freeze": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", @@ -10699,6 +10781,38 @@ "react-native": "*" } }, + "node_modules/react-native-web": { + "version": "0.19.13", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz", + "integrity": "sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@react-native/normalize-colors": "^0.74.1", + "fbjs": "^3.0.4", + "inline-style-prefixer": "^6.0.1", + "memoize-one": "^6.0.0", + "nullthrows": "^1.1.1", + "postcss-value-parser": "^4.2.0", + "styleq": "^0.1.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/react-native-web/node_modules/@react-native/normalize-colors": { + "version": "0.74.89", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz", + "integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==", + "license": "MIT" + }, + "node_modules/react-native-web/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz", @@ -11765,6 +11879,12 @@ "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==", "license": "MIT" }, + "node_modules/styleq": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz", + "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", diff --git a/package.json b/package.json index 3e4f3b1..6d220d7 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,10 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-svg": "^15.8.0", - "react-native-video": "^6.12.0" + "react-native-video": "^6.12.0", + "expo-intent-launcher": "~12.0.2", + "react-native-web": "~0.19.13", + "@expo/metro-runtime": "~4.0.1" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index efab916..6e4ad48 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -111,7 +111,68 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = const [recommendations, setRecommendations] = useState([]); const [loadingRecommendations, setLoadingRecommendations] = useState(false); - const processStreamSource = async (sourceType: string, promise: Promise, isEpisode = false) => { + const processStremioSource = async (type: string, id: string, isEpisode = false) => { + const sourceStartTime = Date.now(); + const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams'; + const sourceName = 'stremio'; + + logger.log(`🔍 [${logPrefix}:${sourceName}] Starting fetch`); + + try { + await stremioService.getStreams(type, id, + (streams, addonId, addonName, error) => { + const processTime = Date.now() - sourceStartTime; + if (error) { + logger.error(`❌ [${logPrefix}:${sourceName}] Error for addon ${addonName} (${addonId}):`, error); + // Optionally update state to show error for this specific addon? + // For now, just log the error. + } else if (streams && addonId && addonName) { + logger.log(`✅ [${logPrefix}:${sourceName}] Received ${streams.length} streams from ${addonName} (${addonId}) after ${processTime}ms`); + if (streams.length > 0) { + const streamsWithAddon = streams.map(stream => ({ + ...stream, + name: stream.name || stream.title || 'Unnamed Stream', + addonId: addonId, + addonName: addonName + })); + + const updateState = (prevState: GroupedStreams): GroupedStreams => { + logger.log(`🔄 [${logPrefix}:${sourceName}] Updating state for addon ${addonName} (${addonId})`); + return { + ...prevState, + [addonId]: { + addonName: addonName, + streams: streamsWithAddon + } + }; + }; + + if (isEpisode) { + setEpisodeStreams(updateState); + } else { + setGroupedStreams(updateState); + } + } else { + logger.log(`🤷 [${logPrefix}:${sourceName}] No streams found for addon ${addonName} (${addonId})`); + } + } else { + // Handle case where callback provides null streams without error (e.g., empty results) + logger.log(`🏁 [${logPrefix}:${sourceName}] Finished fetching for addon ${addonName} (${addonId}) with no streams after ${processTime}ms`); + } + } + ); + // The function now returns void, just await to let callbacks fire + logger.log(`🏁 [${logPrefix}:${sourceName}] Stremio fetching process initiated`); + } catch (error) { + // Catch errors from the initial call to getStreams (e.g., initialization errors) + logger.error(`❌ [${logPrefix}:${sourceName}] Initial call failed:`, error); + // Maybe update state to show a general Stremio error? + } + // Note: This function completes when getStreams returns, not when all callbacks have fired. + // Loading indicators should probably be managed based on callbacks completing. + }; + + const processExternalSource = async (sourceType: string, promise: Promise, isEpisode = false) => { const sourceStartTime = Date.now(); const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams'; @@ -120,35 +181,26 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = const result = await promise; logger.log(`✅ [${logPrefix}:${sourceType}] Completed in ${Date.now() - sourceStartTime}ms`); - // If we have results, update immediately if (Object.keys(result).length > 0) { - // Calculate total streams for logging - const totalStreams = Object.values(result).reduce((acc, group: any) => { - return acc + (group.streams?.length || 0); - }, 0); - + const totalStreams = Object.values(result).reduce((acc, group: any) => acc + (group.streams?.length || 0), 0); logger.log(`đŸ“Ļ [${logPrefix}:${sourceType}] Found ${totalStreams} streams`); - // Update state for this source + const updateState = (prevState: GroupedStreams) => { + logger.log(`🔄 [${logPrefix}:${sourceType}] Updating state with ${Object.keys(result).length} providers`); + return { ...prevState, ...result }; + }; + if (isEpisode) { - setEpisodeStreams(prev => { - const newState = {...prev, ...result}; - console.log(`🔄 [${logPrefix}:${sourceType}] Updating state with ${Object.keys(result).length} providers`); - return newState; - }); + setEpisodeStreams(updateState); } else { - setGroupedStreams(prev => { - const newState = {...prev, ...result}; - console.log(`🔄 [${logPrefix}:${sourceType}] Updating state with ${Object.keys(result).length} providers`); - return newState; - }); + setGroupedStreams(updateState); } } else { - console.log(`âš ī¸ [${logPrefix}:${sourceType}] No streams found`); + logger.log(`âš ī¸ [${logPrefix}:${sourceType}] No streams found`); } return result; } catch (error) { - console.error(`❌ [${logPrefix}:${sourceType}] Error:`, error); + logger.error(`❌ [${logPrefix}:${sourceType}] Error:`, error); return {}; } }; @@ -346,43 +398,17 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = console.log('â„šī¸ [loadStreams] Using ID as TMDB ID:', tmdbId); } - console.log('🔄 [loadStreams] Starting parallel stream requests'); + console.log('🔄 [loadStreams] Starting stream requests'); - // Create an array to store all fetching promises const fetchPromises = []; - // Start Stremio request - const stremioPromise = processStreamSource('stremio', (async () => { - const newGroupedStreams: GroupedStreams = {}; - try { - const responses = await stremioService.getStreams(type, id); - responses.forEach(response => { - const addonId = response.addon; - if (addonId && response.streams.length > 0) { - const streamsWithAddon = response.streams.map(stream => ({ - ...stream, - name: stream.name || stream.title || 'Unnamed Stream', - addonId: response.addon, - addonName: response.addonName - })); - - newGroupedStreams[addonId] = { - addonName: response.addonName, - streams: streamsWithAddon - }; - } - }); - return newGroupedStreams; - } catch (error) { - console.error('❌ [loadStreams:stremio] Error fetching Stremio streams:', error); - return {}; - } - })(), false); - fetchPromises.push(stremioPromise); + // Start Stremio request using the new callback method + // We don't push this promise anymore, as results are handled by callback + processStremioSource(type, id, false); // Start Source 1 request if we have a TMDB ID if (tmdbId) { - const source1Promise = processStreamSource('source1', (async () => { + const source1Promise = processExternalSource('source1', (async () => { try { const streams = await fetchExternalStreams( `https://nice-month-production.up.railway.app/embedsu/${tmdbId}`, @@ -408,7 +434,7 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = // Start Source 2 request if we have a TMDB ID if (tmdbId) { - const source2Promise = processStreamSource('source2', (async () => { + const source2Promise = processExternalSource('source2', (async () => { try { const streams = await fetchExternalStreams( `https://vidsrc-api-js-phz6.onrender.com/embedsu/${tmdbId}`, @@ -432,12 +458,12 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = fetchPromises.push(source2Promise); } - // Wait for all promises to complete - but we already showed results as they arrived + // Wait only for external promises now const results = await Promise.allSettled(fetchPromises); const totalTime = Date.now() - startTime; - console.log(`✅ [loadStreams] All requests completed in ${totalTime}ms`); + console.log(`✅ [loadStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`); - const sourceTypes = ['stremio', 'source1', 'source2']; + const sourceTypes = ['source1', 'source2']; // Removed 'stremio' results.forEach((result, index) => { const source = sourceTypes[Math.min(index, sourceTypes.length - 1)]; console.log(`📊 [loadStreams:${source}] Status: ${result.status}`); @@ -447,18 +473,19 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = }); console.log('🧮 [loadStreams] Summary:'); - console.log(' Total time:', totalTime + 'ms'); + console.log(' Total time for external sources:', totalTime + 'ms'); - // Log the final states - console.log('đŸ“Ļ [loadStreams] Final streams count:', + // Log the final states - this might not include all Stremio addons yet + console.log('đŸ“Ļ [loadStreams] Current combined streams count:', Object.keys(groupedStreams).length > 0 ? Object.values(groupedStreams).reduce((acc, group: any) => acc + group.streams.length, 0) : 0 ); - // Cache the final streams state + // Cache the final streams state - Note: This might be incomplete if Stremio addons are slow setGroupedStreams(prev => { - cacheService.setStreams(id, type, prev); + // We might want to reconsider when exactly to cache or mark loading as fully complete + // cacheService.setStreams(id, type, prev); // Maybe cache incrementally in callback? setPreloadedStreams(prev); return prev; }); @@ -467,9 +494,11 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = console.error('❌ [loadStreams] Failed to load streams:', error); setError('Failed to load streams'); } finally { + // Loading is now complete when external sources finish, Stremio updates happen independently. + // We need a better way to track overall completion if we want a final 'FINISHED' log. const endTime = Date.now() - startTime; - console.log(`🏁 [loadStreams] FINISHED in ${endTime}ms`); - setLoadingStreams(false); + console.log(`🏁 [loadStreams] External sources FINISHED in ${endTime}ms`); + setLoadingStreams(false); // Mark loading=false, but Stremio might still be working } }; @@ -500,43 +529,17 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = const episodeQuery = `?s=${season}&e=${episode}`; console.log(`â„šī¸ [loadEpisodeStreams] Episode query: ${episodeQuery}`); - console.log('🔄 [loadEpisodeStreams] Starting parallel stream requests'); + console.log('🔄 [loadEpisodeStreams] Starting stream requests'); - // Create an array to store all fetching promises const fetchPromises = []; - // Start Stremio request - const stremioPromise = processStreamSource('stremio', (async () => { - const newGroupedStreams: GroupedStreams = {}; - try { - const responses = await stremioService.getStreams('series', episodeId); - responses.forEach(response => { - const addonId = response.addon; - if (addonId && response.streams.length > 0) { - const streamsWithAddon = response.streams.map(stream => ({ - ...stream, - name: stream.name || stream.title || 'Unnamed Stream', - addonId: response.addon, - addonName: response.addonName - })); - - newGroupedStreams[addonId] = { - addonName: response.addonName, - streams: streamsWithAddon - }; - } - }); - return newGroupedStreams; - } catch (error) { - console.error('❌ [loadEpisodeStreams:stremio] Error fetching Stremio streams:', error); - return {}; - } - })(), true); - fetchPromises.push(stremioPromise); + // Start Stremio request using the new callback method + // We don't push this promise anymore + processStremioSource('series', episodeId, true); // Start Source 1 request if we have a TMDB ID if (tmdbId) { - const source1Promise = processStreamSource('source1', (async () => { + const source1Promise = processExternalSource('source1', (async () => { try { const streams = await fetchExternalStreams( `https://nice-month-production.up.railway.app/embedsu/${tmdbId}${episodeQuery}`, @@ -563,7 +566,7 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = // Start Source 2 request if we have a TMDB ID if (tmdbId) { - const source2Promise = processStreamSource('source2', (async () => { + const source2Promise = processExternalSource('source2', (async () => { try { const streams = await fetchExternalStreams( `https://vidsrc-api-js-phz6.onrender.com/embedsu/${tmdbId}${episodeQuery}`, @@ -588,12 +591,12 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = fetchPromises.push(source2Promise); } - // Wait for all promises to complete - but we already showed results as they arrived + // Wait only for external promises now const results = await Promise.allSettled(fetchPromises); const totalTime = Date.now() - startTime; - console.log(`✅ [loadEpisodeStreams] All requests completed in ${totalTime}ms`); + console.log(`✅ [loadEpisodeStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`); - const sourceTypes = ['stremio', 'source1', 'source2']; + const sourceTypes = ['source1', 'source2']; // Removed 'stremio' results.forEach((result, index) => { const source = sourceTypes[Math.min(index, sourceTypes.length - 1)]; console.log(`📊 [loadEpisodeStreams:${source}] Status: ${result.status}`); @@ -603,18 +606,18 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = }); console.log('🧮 [loadEpisodeStreams] Summary:'); - console.log(' Total time:', totalTime + 'ms'); + console.log(' Total time for external sources:', totalTime + 'ms'); - // Log the final states - console.log('đŸ“Ļ [loadEpisodeStreams] Final streams count:', + // Log the final states - might not include all Stremio addons yet + console.log('đŸ“Ļ [loadEpisodeStreams] Current combined streams count:', Object.keys(episodeStreams).length > 0 ? - Object.values(episodeStreams).reduce((acc, group: any) => acc + group.streams.length, 0) : + Object.values(episodeStreams).reduce((acc, group: any) => acc + group.streams.length, 0) : 0 ); - // Cache the final streams state + // Cache the final streams state - Might be incomplete setEpisodeStreams(prev => { - // Cache episode streams + // Cache episode streams - maybe incrementally? setPreloadedEpisodeStreams(currentPreloaded => ({ ...currentPreloaded, [episodeId]: prev @@ -626,9 +629,10 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = console.error('❌ [loadEpisodeStreams] Failed to load episode streams:', error); setError('Failed to load episode streams'); } finally { - const totalTime = Date.now() - startTime; - console.log(`🏁 [loadEpisodeStreams] FINISHED in ${totalTime}ms`); - setLoadingEpisodeStreams(false); + // Loading is now complete when external sources finish + const endTime = Date.now() - startTime; + console.log(`🏁 [loadEpisodeStreams] External sources FINISHED in ${endTime}ms`); + setLoadingEpisodeStreams(false); // Mark loading=false, but Stremio might still be working } }; diff --git a/src/screens/CatalogScreen.tsx b/src/screens/CatalogScreen.tsx index dc02320..43ad0f1 100644 --- a/src/screens/CatalogScreen.tsx +++ b/src/screens/CatalogScreen.tsx @@ -11,8 +11,7 @@ import { RefreshControl, Dimensions, } from 'react-native'; -import { RouteProp, useRoute, useNavigation } from '@react-navigation/native'; -import { MaterialIcons } from '@expo/vector-icons'; +import { RouteProp } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; import { RootStackParamList } from '../navigation/AppNavigator'; import { Meta, stremioService } from '../services/stremioService'; @@ -25,9 +24,20 @@ type CatalogScreenProps = { navigation: StackNavigationProp; }; +// Consistent spacing variables +const SPACING = { + xs: 4, + sm: 8, + md: 12, + lg: 16, + xl: 24, +}; + +// Screen dimensions and grid layout const { width } = Dimensions.get('window'); const NUM_COLUMNS = 3; -const ITEM_WIDTH = width / NUM_COLUMNS - 20; +const ITEM_MARGIN = SPACING.sm; +const ITEM_WIDTH = (width - (SPACING.md * 2) - (ITEM_MARGIN * 2 * NUM_COLUMNS)) / NUM_COLUMNS; const CatalogScreen: React.FC = ({ route, navigation }) => { const { addonId, type, id, name, genreFilter } = route.params; @@ -172,58 +182,85 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { navigation.navigate('Metadata', { id: item.id, type: item.type })} + activeOpacity={0.7} > - - {item.name} - - {item.releaseInfo && ( + - {item.releaseInfo} + {item.name} - )} + {item.releaseInfo && ( + + {item.releaseInfo} + + )} + ); }, [navigation]); + const renderEmptyState = () => ( + + + No content found for the selected genre + + + Try Again + + + ); + + const renderErrorState = () => ( + + + {error} + + loadItems(1)} + > + Retry + + + ); + + const renderLoadingState = () => ( + + + + ); + if (loading && items.length === 0) { return ( - - - + + + {renderLoadingState()} + ); } if (error && items.length === 0) { return ( - - - {error} - - loadItems(1)} - > - Retry - - + + + {renderErrorState()} + ); } return ( - + {items.length > 0 ? ( = ({ route, navigation }) => { ) : null } contentContainerStyle={styles.list} + columnWrapperStyle={styles.columnWrapper} /> - ) : ( - - - No content found for the selected genre - - - Try Again - - - )} + ) : renderEmptyState()} ); }; @@ -272,59 +298,73 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: colors.darkBackground, }, - header: { - padding: 16, - borderBottomWidth: 1, - borderBottomColor: 'rgba(255,255,255,0.1)', - }, - headerTitle: { - fontSize: 18, - fontWeight: 'bold', - color: colors.white, - }, list: { - padding: 10, + padding: SPACING.md, + }, + columnWrapper: { + justifyContent: 'space-between', }, item: { width: ITEM_WIDTH, - margin: 5, + marginBottom: SPACING.md, + borderRadius: 8, overflow: 'hidden', }, poster: { width: '100%', aspectRatio: 2/3, - borderRadius: 4, + borderRadius: 8, backgroundColor: colors.transparentLight, }, + itemContent: { + padding: SPACING.xs, + }, title: { - marginTop: 5, + marginTop: SPACING.xs, fontSize: 14, - fontWeight: '500', + fontWeight: '600', color: colors.white, + lineHeight: 18, }, releaseInfo: { fontSize: 12, - marginTop: 2, + marginTop: SPACING.xs, color: colors.lightGray, }, footer: { - padding: 20, + padding: SPACING.lg, alignItems: 'center', }, - retryButton: { - marginTop: 15, - padding: 10, + button: { + marginTop: SPACING.md, + paddingVertical: SPACING.md, + paddingHorizontal: SPACING.xl, backgroundColor: colors.primary, - borderRadius: 5, + borderRadius: 8, + elevation: 2, }, - retryText: { + buttonText: { color: colors.white, - fontWeight: '500', + fontWeight: '600', + fontSize: 16, }, centered: { + flex: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: colors.darkBackground, + padding: SPACING.xl, + }, + emptyText: { + color: colors.white, + fontSize: 16, + textAlign: 'center', + marginBottom: SPACING.md, + }, + errorText: { + color: colors.white, + fontSize: 16, + textAlign: 'center', + marginBottom: SPACING.md, }, }); diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index f6696e0..8cd7c20 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -770,7 +770,12 @@ const HomeScreen = () => { {item.name} - + @@ -1041,9 +1046,8 @@ const styles = StyleSheet.create({ position: 'absolute', bottom: -4, left: 0, - width: 40, + width: 60, height: 3, - backgroundColor: colors.primary, borderRadius: 1.5, }, seeAllButton: { diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 853c20b..43b035b 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -1123,22 +1123,17 @@ const styles = StyleSheet.create({ ratingContainer: { flexDirection: 'row', alignItems: 'center', - backgroundColor: colors.elevation3, - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 4, - borderWidth: 1, - borderColor: 'rgba(255,255,255,0.1)', }, imdbLogo: { - width: 40, - height: 20, - marginRight: 6, + width: 35, + height: 18, + marginRight: 4, }, ratingText: { - color: '#fff', + color: colors.text, fontWeight: '700', - fontSize: 13, + fontSize: 15, + letterSpacing: 0.3, }, descriptionContainer: { marginBottom: 16, diff --git a/src/screens/PlayerScreen.tsx b/src/screens/PlayerScreen.tsx deleted file mode 100644 index 82d0dbb..0000000 --- a/src/screens/PlayerScreen.tsx +++ /dev/null @@ -1,357 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { - View, - StyleSheet, - TouchableOpacity, - Text, - ActivityIndicator, - StatusBar, - Dimensions, - Platform, - SafeAreaView -} from 'react-native'; -import Video from 'react-native-video'; -import { RouteProp, useRoute, useNavigation } from '@react-navigation/native'; -import { MaterialIcons } from '@expo/vector-icons'; - -interface PlayerParams { - id: string; - type: string; - title?: string; - poster?: string; - stream?: string; -} - -const PlayerScreen = () => { - const route = useRoute, string>>(); - const navigation = useNavigation(); - const { id, type, title, poster, stream } = route.params; - - const [isPlaying, setIsPlaying] = useState(true); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [currentTime, setCurrentTime] = useState(0); - const [duration, setDuration] = useState(0); - const [controlsVisible, setControlsVisible] = useState(true); - - // Use any for now to fix the type error - const videoRef = useRef(null); - const controlsTimeoutRef = useRef(null); - - // Auto-hide controls after a delay - useEffect(() => { - if (controlsVisible) { - if (controlsTimeoutRef.current) { - clearTimeout(controlsTimeoutRef.current); - } - - controlsTimeoutRef.current = setTimeout(() => { - setControlsVisible(false); - }, 3000); - } - - return () => { - if (controlsTimeoutRef.current) { - clearTimeout(controlsTimeoutRef.current); - } - }; - }, [controlsVisible]); - - // Cleanup on unmount - useEffect(() => { - return () => setIsPlaying(false); - }, []); - - const toggleControls = () => { - setControlsVisible(!controlsVisible); - }; - - const togglePlayPause = () => { - setIsPlaying(!isPlaying); - }; - - const seekTo = (time: number) => { - if (videoRef.current) { - videoRef.current.seek(time); - } - }; - - const formatTime = (seconds: number): string => { - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds % 3600) / 60); - const s = Math.floor(seconds % 60); - - let result = ''; - if (h > 0) { - result += `${h}:${m < 10 ? '0' : ''}`; - } - result += `${m}:${s < 10 ? '0' : ''}${s}`; - - return result; - }; - - const handleLoad = (data: any) => { - setDuration(data.duration); - setIsLoading(false); - }; - - const handleProgress = (data: any) => { - setCurrentTime(data.currentTime); - }; - - const handleEnd = () => { - setIsPlaying(false); - setCurrentTime(duration); - }; - - const handleError = (err: any) => { - setError(err.error?.errorString || 'Failed to load video'); - setIsLoading(false); - }; - - const GoBackButton = () => ( - navigation.goBack()} - > - - - ); - - const Controls = () => ( - - - - {title || 'Video Player'} - - - - - - - - - - {formatTime(currentTime)} - - - - - - - {formatTime(duration)} - - - ); - - if (!stream) { - return ( - - - - - - No stream URL provided - navigation.goBack()} - > - Go Back - - - - ); - } - - return ( - - - - - - - ); -}; - -const { width, height } = Dimensions.get('window'); - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000000', - }, - videoContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - video: { - position: 'absolute', - top: 0, - left: 0, - bottom: 0, - right: 0, - }, - controlsContainer: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - justifyContent: 'space-between', - }, - controlsHeader: { - flexDirection: 'row', - alignItems: 'center', - padding: 16, - paddingTop: Platform.OS === 'ios' ? 50 : 40, - }, - backButton: { - width: 40, - height: 40, - justifyContent: 'center', - alignItems: 'center', - marginRight: 16, - }, - videoTitle: { - color: '#FFFFFF', - fontSize: 18, - fontWeight: 'bold', - flex: 1, - }, - controlsCenter: { - alignItems: 'center', - justifyContent: 'center', - }, - playPauseButton: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - justifyContent: 'center', - alignItems: 'center', - }, - controlsBottom: { - flexDirection: 'row', - alignItems: 'center', - padding: 16, - paddingBottom: Platform.OS === 'ios' ? 40 : 16, - }, - timeText: { - color: '#FFFFFF', - fontSize: 14, - width: 50, - }, - progressBarContainer: { - flex: 1, - height: 4, - backgroundColor: 'rgba(255, 255, 255, 0.3)', - marginHorizontal: 8, - borderRadius: 2, - position: 'relative', - }, - progressBar: { - height: '100%', - backgroundColor: '#E50914', - borderRadius: 2, - }, - progressKnob: { - width: 12, - height: 12, - borderRadius: 6, - backgroundColor: '#E50914', - position: 'absolute', - top: -4, - marginLeft: -6, - }, - loaderContainer: { - ...StyleSheet.absoluteFillObject, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.7)', - }, - errorContainer: { - ...StyleSheet.absoluteFillObject, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.9)', - padding: 20, - }, - errorText: { - color: '#FFFFFF', - fontSize: 16, - textAlign: 'center', - marginTop: 16, - marginBottom: 20, - }, - errorButton: { - backgroundColor: '#E50914', - paddingVertical: 12, - paddingHorizontal: 24, - borderRadius: 8, - }, - errorButtonText: { - color: '#FFFFFF', - fontWeight: 'bold', - fontSize: 16, - }, -}); - -export default PlayerScreen; \ No newline at end of file diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 056636a..9034e33 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -20,6 +20,7 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import { colors } from '../styles/colors'; import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings'; import { RootStackParamList } from '../navigation/AppNavigator'; +import { stremioService } from '../services/stremioService'; const { width } = Dimensions.get('window'); @@ -223,6 +224,66 @@ const SettingsScreen: React.FC = () => { )} onPress={() => navigation.navigate('Addons')} /> + ( + + Check + + )} + onPress={() => { + // Check if the addon is installed + const installedAddons = stremioService.getInstalledAddons(); + const tmdbAddon = installedAddons.find(addon => addon.id === 'org.tmdbembedapi'); + + if (tmdbAddon) { + // Addon is installed, check its configuration + Alert.alert( + 'TMDB Embed Streams Addon', + `Addon is installed:\n\nName: ${tmdbAddon.name}\nID: ${tmdbAddon.id}\nURL: ${tmdbAddon.url}\n\nResources: ${JSON.stringify(tmdbAddon.resources)}\n\nTypes: ${JSON.stringify(tmdbAddon.types)}`, + [ + { + text: 'Reinstall', + onPress: async () => { + try { + // Remove and reinstall the addon + stremioService.removeAddon('org.tmdbembedapi'); + await stremioService.installAddon('https://http-addon-production.up.railway.app/manifest.json'); + Alert.alert('Success', 'Addon was reinstalled successfully'); + } catch (error) { + Alert.alert('Error', `Failed to reinstall addon: ${error}`); + } + } + }, + { text: 'Close', style: 'cancel' } + ] + ); + } else { + // Addon is not installed, offer to install it + Alert.alert( + 'TMDB Embed Streams Addon', + 'Addon is not installed. Would you like to install it now?', + [ + { + text: 'Install', + onPress: async () => { + try { + await stremioService.installAddon('https://http-addon-production.up.railway.app/manifest.json'); + Alert.alert('Success', 'Addon was installed successfully'); + } catch (error) { + Alert.alert('Error', `Failed to install addon: ${error}`); + } + } + }, + { text: 'Cancel', style: 'cancel' } + ] + ); + } + }} + /> - FadeInDown - .delay(50 + Math.min(index, 10) * 30) - .springify() - .damping(15) - .mass(0.9) - , [index]); - - const handlePress = useCallback(() => { - logger.log('StreamCard pressed:', { - isTorrent, - isDebrid, - hasProgress: !!torrentProgress, - url: stream.url, - behaviorHints: stream.behaviorHints - }); - onPress(); - }, [isTorrent, isDebrid, torrentProgress, stream.url, stream.behaviorHints, onPress]); - // Only disable if it's a torrent that's not debrid and not currently downloading const isDisabled = isTorrent && !isDebrid && !torrentProgress && !stream.behaviorHints?.notWebReady; @@ -95,107 +76,109 @@ const StreamCard = memo(({ stream, onPress, index, torrentProgress, isLoading, s const isDownloading = !!torrentProgress && isTorrent; return ( - - - - - - - {displayTitle} + + + + + + {displayTitle} + + {displayAddonName && displayAddonName !== displayTitle && ( + + {displayAddonName} - {displayAddonName && displayAddonName !== displayTitle && ( - - {displayAddonName} - - )} - - - {/* Show loading indicator if stream is loading */} - {isLoading && ( - - - - {statusMessage || "Loading..."} - - - )} - - {/* Show download indicator for active downloads */} - {isDownloading && ( - - - Downloading... - )} - - {quality && quality >= "720" && ( - - )} - - {isDolby && ( - - )} - - {size && ( - - {size} - - )} - - {isTorrent && !isDebrid && ( - - TORRENT - - )} - - {isDebrid && ( - - DEBRID - - )} - - - {/* Render progress bar if there's progress */} - {torrentProgress && ( - - - - {`${Math.round(torrentProgress.bufferProgress)}% â€ĸ ${Math.round(torrentProgress.downloadSpeed / 1024)} KB/s â€ĸ ${torrentProgress.seeds} seeds`} + {/* Show loading indicator if stream is loading */} + {isLoading && ( + + + + {statusMessage || "Loading..."} )} + + {/* Show download indicator for active downloads */} + {isDownloading && ( + + + Downloading... + + )} - - + + {quality && quality >= "720" && ( + + )} + + {isDolby && ( + + )} + + {size && ( + + {size} + + )} + + {isTorrent && !isDebrid && ( + + TORRENT + + )} + + {isDebrid && ( + + DEBRID + + )} - - + + {/* Render progress bar if there's progress */} + {torrentProgress && ( + + + + {`${Math.round(torrentProgress.bufferProgress)}% â€ĸ ${Math.round(torrentProgress.downloadSpeed / 1024)} KB/s â€ĸ ${torrentProgress.seeds} seeds`} + + + )} + + + + + + ); }, (prevProps, nextProps) => { - // Custom comparison to prevent unnecessary re-renders - return prevProps.stream.url === nextProps.stream.url && - prevProps.index === nextProps.index && - prevProps.torrentProgress?.bufferProgress === nextProps.torrentProgress?.bufferProgress; + // Simplified memo comparison that won't interfere with onPress + return ( + prevProps.stream.url === nextProps.stream.url && + prevProps.isLoading === nextProps.isLoading && + prevProps.torrentProgress?.bufferProgress === nextProps.torrentProgress?.bufferProgress && + prevProps.statusMessage === nextProps.statusMessage + ); }); const QualityTag = React.memo(({ text, color }: { text: string; color: string }) => ( @@ -260,6 +243,26 @@ export const StreamsScreen = () => { const { id, type, episodeId } = route.params; const { settings } = useSettings(); + // Log the stream details and installed addons for debugging + useEffect(() => { + // Log installed addons + const installedAddons = stremioService.getInstalledAddons(); + console.log('đŸ“Ļ [StreamsScreen] INSTALLED ADDONS:', installedAddons.map(addon => ({ + id: addon.id, + name: addon.name, + version: addon.version, + resources: addon.resources, + types: addon.types + }))); + + // Log request details + console.log('đŸŽŦ [StreamsScreen] REQUEST DETAILS:', { + id, + type, + episodeId: episodeId || 'none' + }); + }, [id, type, episodeId]); + // Add timing logs const [loadStartTime, setLoadStartTime] = useState(0); const [providerLoadTimes, setProviderLoadTimes] = useState<{[key: string]: number}>({}); @@ -278,6 +281,20 @@ export const StreamsScreen = () => { groupedEpisodes, } = useMetadata({ id, type }); + // Log stream results when they arrive + useEffect(() => { + const streams = type === 'series' ? episodeStreams : groupedStreams; + console.log('🔍 [StreamsScreen] STREAM RESULTS:', { + totalProviders: Object.keys(streams).length, + providers: Object.keys(streams), + streamCounts: Object.entries(streams).map(([provider, data]) => ({ + provider, + addonName: data.addonName, + streams: data.streams.length + })) + }); + }, [episodeStreams, groupedStreams, type]); + const [selectedProvider, setSelectedProvider] = React.useState('all'); const [availableProviders, setAvailableProviders] = React.useState>(new Set()); @@ -555,18 +572,17 @@ export const StreamsScreen = () => { if (settings.useExternalPlayer) { logger.log('Using external player for URL:', stream.url); // Use VideoPlayerService to launch external player - try { - const videoPlayerService = VideoPlayerService; - await videoPlayerService.playVideo(stream.url, { - useExternalPlayer: true, - title: metadata?.name || '', - episodeTitle: type === 'series' ? currentEpisode?.name : undefined, - episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined, - releaseDate: metadata?.year?.toString(), - }); - } catch (externalPlayerError) { - logger.error('External player error:', externalPlayerError); - // Fallback to built-in player if external player fails + const videoPlayerService = VideoPlayerService; + const launched = await videoPlayerService.playVideo(stream.url, { + useExternalPlayer: true, + title: metadata?.name || '', + episodeTitle: type === 'series' ? currentEpisode?.name : undefined, + episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined, + releaseDate: metadata?.year?.toString(), + }); + + if (!launched) { + logger.log('External player launch failed, falling back to built-in player'); navigation.navigate('Player', { uri: stream.url, title: metadata?.name || '', @@ -695,18 +711,17 @@ export const StreamsScreen = () => { if (settings.useExternalPlayer) { logger.log('[StreamsScreen] Using external player for torrent video path:', videoPath); // Use VideoPlayerService to launch external player - try { - const videoPlayerService = VideoPlayerService; - await videoPlayerService.playVideo(`file://${videoPath}`, { - useExternalPlayer: true, - title: metadata?.name || '', - episodeTitle: type === 'series' ? currentEpisode?.name : undefined, - episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined, - releaseDate: metadata?.year?.toString(), - }); - } catch (externalPlayerError) { - logger.error('[StreamsScreen] External player error:', externalPlayerError); - // Fallback to built-in player if external player fails + const videoPlayerService = VideoPlayerService; + const launched = await videoPlayerService.playVideo(`file://${videoPath}`, { + useExternalPlayer: true, + title: metadata?.name || '', + episodeTitle: type === 'series' ? currentEpisode?.name : undefined, + episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined, + releaseDate: metadata?.year?.toString(), + }); + + if (!launched) { + logger.log('[StreamsScreen] External player launch failed, falling back to built-in player'); navigation.navigate('Player', { uri: `file://${videoPath}`, title: metadata?.name || '', @@ -869,6 +884,7 @@ export const StreamsScreen = () => { return ( handleStreamPress(stream)} index={index} @@ -881,7 +897,7 @@ export const StreamsScreen = () => { const renderSectionHeader = useCallback(({ section }: { section: { title: string } }) => ( {section.title} @@ -1060,7 +1076,7 @@ const styles = StyleSheet.create({ top: 0, left: 0, right: 0, - zIndex: 1, + zIndex: 2, }, backButton: { flexDirection: 'row', @@ -1078,6 +1094,7 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: colors.darkBackground, paddingTop: 20, + zIndex: 0, }, streamsMainContentMovie: { paddingTop: Platform.OS === 'android' ? 90 : 100, @@ -1113,6 +1130,7 @@ const styles = StyleSheet.create({ streamsContent: { flex: 1, width: '100%', + zIndex: 1, }, streamsContainer: { paddingHorizontal: 16, @@ -1142,6 +1160,7 @@ const styles = StyleSheet.create({ borderWidth: 1, borderColor: 'rgba(255,255,255,0.05)', width: '100%', + zIndex: 1, }, streamCardDisabled: { backgroundColor: colors.elevation2, diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 34a88fb..6a329c6 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -48,6 +48,11 @@ export interface StreamResponse { addonName: string; } +// Modify the callback signature to include addon ID +interface StreamCallback { + (streams: Stream[] | null, addonId: string | null, addonName: string | null, error: Error | null): void; +} + interface CatalogFilter { title: string; value: any; @@ -446,95 +451,112 @@ class StremioService { } } - async getStreams(type: string, id: string, callback?: (streams: Stream[] | null, addonName: string | null, error: Error | null) => void): Promise { + // Modify getStreams to use the new callback signature and rely on callbacks for results + async getStreams(type: string, id: string, callback?: StreamCallback): Promise { await this.ensureInitialized(); const addons = this.getInstalledAddons(); - logger.log('Installed addons:', addons.map(a => ({ id: a.id, url: a.url }))); + logger.log('📌 [getStreams] Installed addons:', addons.map(a => ({ id: a.id, name: a.name, url: a.url }))); - const streamResponses: StreamResponse[] = []; + // Check specifically for TMDB Embed addon + const tmdbEmbed = addons.find(addon => addon.id === 'org.tmdbembedapi'); + if (tmdbEmbed) { + logger.log('🔍 [getStreams] Found TMDB Embed Streams addon:', { + id: tmdbEmbed.id, + name: tmdbEmbed.name, + url: tmdbEmbed.url, + resources: tmdbEmbed.resources, + types: tmdbEmbed.types + }); + } else { + logger.log('âš ī¸ [getStreams] TMDB Embed Streams addon not found among installed addons'); + } // Find addons that provide streams and sort them by installation order const streamAddons = addons .filter(addon => { if (!addon.resources) { - logger.log(`Addon ${addon.id} has no resources`); + logger.log(`âš ī¸ [getStreams] Addon ${addon.id} has no resources`); return false; } + // Log the detailed resources structure for debugging + // logger.log(`📋 [getStreams] Checking addon ${addon.id} resources:`, JSON.stringify(addon.resources)); // Verbose, uncomment if needed + + // Check if the addon has a stream resource for this type const hasStreamResource = addon.resources.some( - resource => resource.name === 'stream' && resource.types.includes(type) + 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; + } ); if (!hasStreamResource) { - logger.log(`Addon ${addon.id} does not support streaming ${type}`); + // logger.log(`❌ [getStreams] Addon ${addon.id} does not support streaming ${type}`); // Verbose + } else { + // logger.log(`✅ [getStreams] Addon ${addon.id} supports streaming ${type}`); // Verbose } return hasStreamResource; }); - logger.log('Stream capable addons:', streamAddons.map(a => a.id)); + logger.log('📊 [getStreams] Stream capable addons:', streamAddons.map(a => a.id)); if (streamAddons.length === 0) { - logger.warn('No addons found that can provide streams'); - return []; + logger.warn('âš ī¸ [getStreams] No addons found that can provide streams'); + // Optionally call callback with an empty result or specific status? + // For now, just return if no addons. + return; } - // Create a map to store promises for each addon - const addonPromises = new Map>(); - - // Process each addon - for (const addon of streamAddons) { - const promise = (async () => { + // Process each addon and call the callback individually + streamAddons.forEach(addon => { + // Use an IIFE to create scope for async operation inside forEach + (async () => { try { if (!addon.url) { - logger.warn(`Addon ${addon.id} has no URL`); + logger.warn(`âš ī¸ [getStreams] Addon ${addon.id} has no URL`); + if (callback) callback(null, addon.id, addon.name, new Error('Addon has no URL')); return; } const baseUrl = this.getAddonBaseURL(addon.url); const url = `${baseUrl}/stream/${type}/${id}.json`; + logger.log(`🔗 [getStreams] Requesting streams from ${addon.name} (${addon.id}): ${url}`); + const response = await this.retryRequest(async () => { return await axios.get(url); }); + let processedStreams: Stream[] = []; if (response.data && response.data.streams) { - const processedStreams = this.processStreams(response.data.streams, addon); - if (processedStreams.length > 0) { - streamResponses.push({ - addon: addon.id, - addonName: addon.name, - streams: processedStreams - }); - } + logger.log(`✅ [getStreams] Got ${response.data.streams.length} streams from ${addon.name} (${addon.id})`); + processedStreams = this.processStreams(response.data.streams, addon); + logger.log(`✅ [getStreams] Processed ${processedStreams.length} valid streams from ${addon.name} (${addon.id})`); + } else { + logger.log(`âš ī¸ [getStreams] No streams found in response from ${addon.name} (${addon.id})`); } if (callback) { - callback(response.data?.streams || null, addon.name, null); + // Call callback with processed streams (can be empty array) + callback(processedStreams, addon.id, addon.name, null); } } catch (error) { - logger.error(`Failed to get streams from ${addon.name}:`, error); + logger.error(`❌ [getStreams] Failed to get streams from ${addon.name} (${addon.id}):`, error); if (callback) { - callback(null, addon.name, error as Error); + // Call callback with error + callback(null, addon.id, addon.name, error as Error); } } - })(); - - addonPromises.set(addon.id, promise); - } - - // Wait for all promises to complete - await Promise.all(addonPromises.values()); - - // Sort stream responses to maintain installed addon order - streamResponses.sort((a, b) => { - const indexA = streamAddons.findIndex(addon => addon.id === a.addon); - const indexB = streamAddons.findIndex(addon => addon.id === b.addon); - return indexA - indexB; + })(); // Immediately invoke the async function }); - return streamResponses; + // No longer waiting here, callbacks handle results asynchronously + // Removed: await Promise.all(addonPromises.values()); + // No longer returning aggregated results + // Removed: return streamResponses; } private async fetchStreamsFromAddon(addon: Manifest, type: string, id: string): Promise { diff --git a/src/services/videoPlayerService.ts b/src/services/videoPlayerService.ts index d013bb9..055122b 100644 --- a/src/services/videoPlayerService.ts +++ b/src/services/videoPlayerService.ts @@ -1,7 +1,6 @@ -import { NativeModules } from 'react-native'; -import { useSettings } from '../hooks/useSettings'; - -const { VideoPlayerModule } = NativeModules; +import { Platform } from 'react-native'; +import * as IntentLauncher from 'expo-intent-launcher'; +import { logger } from '../utils/logger'; interface VideoPlayerOptions { useExternalPlayer: boolean; @@ -16,11 +15,35 @@ interface VideoPlayerOptions { } export const VideoPlayerService = { - playVideo: (url: string, options?: Partial): Promise => { - if (options) { - return VideoPlayerModule.playVideo(url, options); - } else { - return VideoPlayerModule.playVideo(url); + playVideo: async (url: string, options?: Partial): Promise => { + if (!options?.useExternalPlayer || Platform.OS !== 'android') { + return false; + } + + try { + // Create a title that includes all relevant metadata + const fullTitle = [ + options.title, + options.episodeNumber, + options.episodeTitle, + options.releaseDate + ].filter(Boolean).join(' - '); + + // Launch the intent to play the video + await IntentLauncher.startActivityAsync('android.intent.action.VIEW', { + data: url, + flags: 1, // FLAG_ACTIVITY_NEW_TASK + type: 'video/*', + extra: { + 'android.intent.extra.TITLE': fullTitle, + 'position': 0, // Start from beginning + }, + }); + + return true; + } catch (error) { + logger.error('Failed to launch external player:', error); + return false; } } }; \ No newline at end of file diff --git a/src/styles/colors.ts b/src/styles/colors.ts index ec83056..f335746 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -1,7 +1,7 @@ // Color palette for the app following Material Design 3 export const colors = { // Primary colors - primary: '#00A4A4', // Brighter, more vibrant teal + primary: '#2d9cdb', // Brighter, more vibrant teal secondary: '#FF6B6B', // Coral color that complements teal // Background colors - Deep dark theme