diff --git a/App.tsx b/App.tsx index df02cd890..bf196542f 100644 --- a/App.tsx +++ b/App.tsx @@ -5,7 +5,7 @@ * @format */ -import React from 'react'; +import React, { useState } from 'react'; import { View, StyleSheet @@ -24,6 +24,7 @@ import { CatalogProvider } from './src/contexts/CatalogContext'; import { GenreProvider } from './src/contexts/GenreContext'; import { TraktProvider } from './src/contexts/TraktContext'; import { ThemeProvider, useTheme } from './src/contexts/ThemeContext'; +import SplashScreen from './src/components/SplashScreen'; // This fixes many navigation layout issues by using native screen containers enableScreens(true); @@ -31,6 +32,7 @@ enableScreens(true); // Inner app component that uses the theme context const ThemedApp = () => { const { currentTheme } = useTheme(); + const [isAppReady, setIsAppReady] = useState(false); // Create custom themes based on current theme const customDarkTheme = { @@ -50,6 +52,11 @@ const ThemedApp = () => { background: currentTheme.colors.darkBackground, } }; + + // Handler for splash screen completion + const handleSplashComplete = () => { + setIsAppReady(true); + }; return ( @@ -62,7 +69,8 @@ const ThemedApp = () => { - + {!isAppReady && } + {isAppReady && } diff --git a/app.json b/app.json index 07c328dc0..453ab6cc3 100644 --- a/app.json +++ b/app.json @@ -5,13 +5,13 @@ "version": "1.0.0", "orientation": "default", "icon": "./assets/icon.png", - "userInterfaceStyle": "light", + "userInterfaceStyle": "dark", "scheme": "stremioexpo", "newArchEnabled": true, "splash": { "image": "./assets/splash-icon.png", "resizeMode": "contain", - "backgroundColor": "#ffffff" + "backgroundColor": "#020404" }, "ios": { "supportsTablet": true, @@ -49,7 +49,12 @@ "WAKE_LOCK" ], "package": "com.nuvio.app", - "enableSplitAPKs": true + "enableSplitAPKs": true, + "versionCode": 1, + "enableProguardInReleaseBuilds": true, + "enableHermes": true, + "enableSeparateBuildPerCPUArchitecture": true, + "enableVectorDrawables": true }, "web": { "favicon": "./assets/favicon.png" diff --git a/eas.json b/eas.json index afd500a03..b208a76df 100644 --- a/eas.json +++ b/eas.json @@ -13,7 +13,12 @@ }, "production": { "autoIncrement": true, - "extends": "apk" + "extends": "apk", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleRelease", + "image": "latest" + } }, "release": { "distribution": "store", @@ -23,7 +28,8 @@ }, "apk": { "android": { - "buildType": "apk" + "buildType": "apk", + "gradleCommand": ":app:assembleRelease" } } }, diff --git a/metro.config.js b/metro.config.js index 62186907c..79fe23f68 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,28 +1,31 @@ const { getDefaultConfig } = require('expo/metro-config'); -module.exports = (() => { - const config = getDefaultConfig(__dirname); +const config = getDefaultConfig(__dirname); - const { transformer, resolver } = config; - - config.transformer = { - ...transformer, - babelTransformerPath: require.resolve('react-native-svg-transformer'), - minifierConfig: { - compress: { - // Remove console.* statements in release builds - drop_console: true, - // Keep error logging for critical issues - pure_funcs: ['console.info', 'console.log', 'console.debug', 'console.warn'], - }, +// Enable tree shaking and better minification +config.transformer = { + ...config.transformer, + babelTransformerPath: require.resolve('react-native-svg-transformer'), + minifierConfig: { + ecma: 8, + keep_fnames: true, + mangle: { + keep_fnames: true, }, - }; + compress: { + drop_console: true, + drop_debugger: true, + pure_funcs: ['console.log', 'console.info', 'console.debug'], + }, + }, +}; - config.resolver = { - ...resolver, - assetExts: resolver.assetExts.filter((ext) => ext !== 'svg'), - sourceExts: [...resolver.sourceExts, 'svg'], - }; +// Optimize resolver for better tree shaking and SVG support +config.resolver = { + ...config.resolver, + assetExts: config.resolver.assetExts.filter((ext) => ext !== 'svg'), + sourceExts: [...config.resolver.sourceExts, 'svg'], + resolverMainFields: ['react-native', 'browser', 'main'], +}; - return config; -})(); \ No newline at end of file +module.exports = config; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 60caca11f..a8a42dd53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@shopify/flash-list": "1.7.3", "@types/lodash": "^4.17.16", "@types/react-native-video": "^5.0.20", - "axios": "^1.8.4", + "axios": "^1.10.0", "base64-js": "^1.5.1", "date-fns": "^4.1.0", "eventemitter3": "^5.0.1", @@ -4927,9 +4927,9 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -5554,21 +5554,21 @@ } }, "node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.0.tgz", + "integrity": "sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==", "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "domutils": "^3.1.0", + "domutils": "^3.2.2", "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", + "undici": "^7.10.0", "whatwg-mimetype": "^4.0.0" }, "engines": { @@ -5595,6 +5595,15 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cheerio/node_modules/undici": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", + "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -6143,9 +6152,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7880,9 +7889,9 @@ "license": "ISC" }, "node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -7894,8 +7903,20 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/http-errors": { @@ -13696,9 +13717,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 7d70e09b1..2fef212a2 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@shopify/flash-list": "1.7.3", "@types/lodash": "^4.17.16", "@types/react-native-video": "^5.0.20", - "axios": "^1.8.4", + "axios": "^1.10.0", "base64-js": "^1.5.1", "date-fns": "^4.1.0", "eventemitter3": "^5.0.1", diff --git a/scripts/test-hdrezka.js b/scripts/test-hdrezka.js index d1e8ab1f6..1d190aa51 100644 --- a/scripts/test-hdrezka.js +++ b/scripts/test-hdrezka.js @@ -1,4 +1,4 @@ -// Test script for HDRezka service +d// Test script for HDRezka service // Run with: node scripts/test-hdrezka.js const fetch = require('node-fetch'); diff --git a/src/components/SplashScreen.tsx b/src/components/SplashScreen.tsx new file mode 100644 index 000000000..fabe7b812 --- /dev/null +++ b/src/components/SplashScreen.tsx @@ -0,0 +1,59 @@ +import React, { useEffect } from 'react'; +import { View, Image, StyleSheet, Animated } from 'react-native'; +import { colors } from '../styles/colors'; + +interface SplashScreenProps { + onFinish: () => void; +} + +const SplashScreen = ({ onFinish }: SplashScreenProps) => { + // Animation value for opacity + const fadeAnim = new Animated.Value(1); + + useEffect(() => { + // Wait for a short period then start fade out animation + const timer = setTimeout(() => { + Animated.timing(fadeAnim, { + toValue: 0, + duration: 800, + useNativeDriver: true, + }).start(() => { + // Call onFinish when animation completes + onFinish(); + }); + }, 1500); // Show splash for 1.5 seconds + + return () => clearTimeout(timer); + }, [fadeAnim, onFinish]); + + return ( + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.darkBackground, + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 10, + }, + image: { + width: '70%', + height: '70%', + }, +}); + +export default SplashScreen; \ No newline at end of file diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 3da4a2989..2655caae0 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -297,16 +297,56 @@ const VideoPlayer: React.FC = () => { const seekToTime = (timeInSeconds: number) => { if (!isPlayerReady || duration <= 0 || !vlcRef.current) return; const normalizedPosition = Math.max(0, Math.min(timeInSeconds / duration, 1)); + try { - if (typeof vlcRef.current.setPosition === 'function') { + if (Platform.OS === 'android') { + // On Android, we need to handle seeking differently to prevent black screens + setIsBuffering(true); + + // Set a small timeout to prevent overwhelming the player + const now = Date.now(); + if (now - lastSeekTime.current < 300) { + // Throttle seeks that are too close together + if (seekDebounceTimer.current) { + clearTimeout(seekDebounceTimer.current); + } + + seekDebounceTimer.current = setTimeout(() => { + if (vlcRef.current) { + // Set position instead of using seek on Android + vlcRef.current.setPosition(normalizedPosition); + lastSeekTime.current = Date.now(); + + // Give the player some time to recover + setTimeout(() => { + setIsBuffering(false); + }, 500); + } + }, 300); + return; + } + + // Directly set position vlcRef.current.setPosition(normalizedPosition); - } else if (typeof vlcRef.current.seek === 'function') { - vlcRef.current.seek(normalizedPosition); + lastSeekTime.current = now; + + // Reset buffering state after a delay + setTimeout(() => { + setIsBuffering(false); + }, 500); } else { - logger.error('[VideoPlayer] No seek method available on VLC player'); + // For iOS, keep the original behavior + if (typeof vlcRef.current.setPosition === 'function') { + vlcRef.current.setPosition(normalizedPosition); + } else if (typeof vlcRef.current.seek === 'function') { + vlcRef.current.seek(normalizedPosition); + } else { + logger.error('[VideoPlayer] No seek method available on VLC player'); + } } } catch (error) { logger.error('[VideoPlayer] Error during seek operation:', error); + setIsBuffering(false); } }; @@ -329,8 +369,18 @@ const VideoPlayer: React.FC = () => { const handleProgressBarDragEnd = () => { setIsDragging(false); if (pendingSeekValue.current !== null) { - seekToTime(pendingSeekValue.current); - pendingSeekValue.current = null; + // For Android, add a small delay to ensure UI updates before the seek happens + if (Platform.OS === 'android') { + setTimeout(() => { + if (pendingSeekValue.current !== null) { + seekToTime(pendingSeekValue.current); + pendingSeekValue.current = null; + } + }, 150); + } else { + seekToTime(pendingSeekValue.current); + pendingSeekValue.current = null; + } } }; @@ -743,35 +793,63 @@ const VideoPlayer: React.FC = () => { logger.log(`[VideoPlayer] Player ready after source change, seeking to position: ${pendingSeek.position}s out of ${duration}s total`); if (pendingSeek.position > 0 && vlcRef.current) { - // Wait longer for the player to be fully ready and stable + // Longer delay for Android to ensure player is stable + const delayTime = Platform.OS === 'android' ? 2500 : 1500; + setTimeout(() => { if (vlcRef.current && duration > 0 && pendingSeek) { logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`); - // Use our existing seekToTime function which handles VLC methods properly - seekToTime(pendingSeek.position); - - // Also update the current time state to reflect the seek - setCurrentTime(pendingSeek.position); - - // Resume playback if it was playing before the source change - if (pendingSeek.shouldPlay) { + if (Platform.OS === 'android') { + // On Android, wait longer and set isBuffering to improve visual feedback + setIsBuffering(true); + + // For Android, use setPosition directly with normalized value + const normalizedPosition = Math.max(0, Math.min(pendingSeek.position / duration, 1)); + vlcRef.current.setPosition(normalizedPosition); + + // Update the current time + setCurrentTime(pendingSeek.position); + + // Give the player time to recover from the seek setTimeout(() => { - logger.log('[VideoPlayer] Resuming playback after seek'); - setPaused(false); - if (vlcRef.current && typeof vlcRef.current.play === 'function') { - vlcRef.current.play(); + setIsBuffering(false); + + // Resume playback after a delay if needed + if (pendingSeek.shouldPlay) { + setPaused(false); } - }, 700); // Wait longer for seek to complete properly + + // Clean up + setPendingSeek(null); + setIsChangingSource(false); + }, 800); + } else { + // iOS - use the normal seekToTime function + seekToTime(pendingSeek.position); + + // Also update the current time state + setCurrentTime(pendingSeek.position); + + // Resume playback if needed + if (pendingSeek.shouldPlay) { + setTimeout(() => { + logger.log('[VideoPlayer] Resuming playback after seek'); + setPaused(false); + if (vlcRef.current && typeof vlcRef.current.play === 'function') { + vlcRef.current.play(); + } + }, 700); + } + + // Clean up + setTimeout(() => { + setPendingSeek(null); + setIsChangingSource(false); + }, 800); } - - // Clean up after a reasonable delay - setTimeout(() => { - setPendingSeek(null); - setIsChangingSource(false); - }, 800); } - }, 1500); // Increased delay to ensure player is fully stable + }, delayTime); } else { // No seeking needed, just resume playback if it was playing if (pendingSeek.shouldPlay) { @@ -963,7 +1041,19 @@ const VideoPlayer: React.FC = () => { }} source={{ uri: currentStreamUrl, - initOptions: [ + initOptions: Platform.OS === 'android' ? [ + '--rtsp-tcp', + '--network-caching=1500', + '--rtsp-caching=1500', + '--no-audio-time-stretch', + '--clock-jitter=0', + '--clock-synchro=0', + '--drop-late-frames', + '--skip-frames', + '--aout=opensles', + '--file-caching=1500', + '--sout-mux-caching=1500', + ] : [ '--rtsp-tcp', '--network-caching=150', '--rtsp-caching=150', @@ -984,6 +1074,7 @@ const VideoPlayer: React.FC = () => { onProgress={handleProgress} onEnd={onEnd} onError={handleError} + onBuffering={onBuffering} /> diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index c6941c12c..fe7113628 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -4,12 +4,13 @@ import { catalogService } from '../services/catalogService'; import { stremioService } from '../services/stremioService'; import { tmdbService } from '../services/tmdbService'; import { hdrezkaService } from '../services/hdrezkaService'; -import { xprimeService } from '../services/xprimeService'; import { cacheService } from '../services/cacheService'; import { Cast, Episode, GroupedEpisodes, GroupedStreams } from '../types/metadata'; import { TMDBService } from '../services/tmdbService'; import { logger } from '../utils/logger'; import { usePersistentSeasons } from './usePersistentSeasons'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { Stream } from '../types/metadata'; // Constants for timeouts and retries const API_TIMEOUT = 10000; // 10 seconds @@ -115,6 +116,8 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = const [recommendations, setRecommendations] = useState([]); const [loadingRecommendations, setLoadingRecommendations] = useState(false); const [imdbId, setImdbId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [availableStreams, setAvailableStreams] = useState<{ [sourceType: string]: Stream }>({}); // Add hook for persistent seasons const { getSeason, saveSeason } = usePersistentSeasons(); @@ -216,78 +219,43 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = } }; - const processXprimeSource = async (type: string, id: string, season?: number, episode?: number, isEpisode = false) => { - const sourceStartTime = Date.now(); - const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams'; - const sourceName = 'xprime'; - - logger.log(`🔍 [${logPrefix}:${sourceName}] Starting fetch`); - - try { - const streams = await xprimeService.getStreams( - id, - type, - season, - episode - ); - - const processTime = Date.now() - sourceStartTime; - - if (streams && streams.length > 0) { - logger.log(`✅ [${logPrefix}:${sourceName}] Received ${streams.length} streams after ${processTime}ms`); - - // Format response similar to Stremio format for the UI - return { - 'xprime': { - addonName: 'XPRIME', - streams - } - }; - } else { - logger.log(`âš ī¸ [${logPrefix}:${sourceName}] No streams found after ${processTime}ms`); - return {}; - } - } catch (error) { - logger.error(`❌ [${logPrefix}:${sourceName}] Error:`, error); - return {}; - } - }; - const processExternalSource = async (sourceType: string, promise: Promise, isEpisode = false) => { - const sourceStartTime = Date.now(); - const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams'; - try { - logger.log(`🔍 [${logPrefix}:${sourceType}] Starting fetch`); + const startTime = Date.now(); const result = await promise; - logger.log(`✅ [${logPrefix}:${sourceType}] Completed in ${Date.now() - sourceStartTime}ms`); + const processingTime = Date.now() - startTime; - if (Object.keys(result).length > 0) { - const totalStreams = Object.values(result).reduce((acc, group: any) => acc + (group.streams?.length || 0), 0); - logger.log(`đŸ“Ļ [${logPrefix}:${sourceType}] Found ${totalStreams} streams`); - + if (result && Object.keys(result).length > 0) { + // Update the appropriate state based on whether this is for an episode or not const updateState = (prevState: GroupedStreams) => { - logger.log(`🔄 [${logPrefix}:${sourceType}] Updating state with ${Object.keys(result).length} providers`); + const newState = { ...prevState }; - // If this is XPRIME, put it first; otherwise append to the end - if (sourceType === 'xprime') { - return { ...result, ...prevState }; - } else { - return { ...prevState, ...result }; - } + // Merge in the new streams + Object.entries(result).forEach(([provider, data]: [string, any]) => { + newState[provider] = data; + }); + + return newState; }; - + if (isEpisode) { setEpisodeStreams(updateState); } else { setGroupedStreams(updateState); } + + console.log(`✅ [processExternalSource:${sourceType}] Processed in ${processingTime}ms, found streams:`, + Object.values(result).reduce((acc: number, curr: any) => acc + (curr.streams?.length || 0), 0) + ); + + // Return the result for the promise chain + return result; } else { - logger.log(`âš ī¸ [${logPrefix}:${sourceType}] No streams found`); + console.log(`âš ī¸ [processExternalSource:${sourceType}] No streams found after ${processingTime}ms`); + return {}; } - return result; } catch (error) { - logger.error(`❌ [${logPrefix}:${sourceType}] Error:`, error); + console.error(`❌ [processExternalSource:${sourceType}] Error:`, error); return {}; } }; @@ -661,9 +629,6 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = 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; @@ -679,27 +644,22 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = tmdbId = id; console.log('â„šī¸ [loadStreams] Using ID as TMDB ID:', tmdbId); } - - console.log('🔄 [loadStreams] Starting stream requests'); // Start Stremio request using the callback method processStremioSource(type, id, false); - - // Add Xprime source (PRIMARY) - const xprimePromise = processExternalSource('xprime', processXprimeSource(type, id), false); // Add HDRezka source const hdrezkaPromise = processExternalSource('hdrezka', processHDRezkaSource(type, id), false); - // Include Xprime and HDRezka in fetchPromises array (Xprime first) - const fetchPromises: Promise[] = [xprimePromise, hdrezkaPromise]; + // Include HDRezka in fetchPromises array + const fetchPromises: Promise[] = [hdrezkaPromise]; // Wait only for external promises now 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: string[] = ['xprime', 'hdrezka']; + const sourceTypes: string[] = ['hdrezka']; results.forEach((result, index) => { const source = sourceTypes[Math.min(index, sourceTypes.length - 1)]; console.log(`📊 [loadStreams:${source}] Status: ${result.status}`); @@ -770,26 +730,20 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn = // Start Stremio request using the callback method processStremioSource('series', episodeId, true); - // Add Xprime source for episodes (PRIMARY) - const xprimeEpisodePromise = processExternalSource('xprime', - processXprimeSource('series', id, parseInt(season), parseInt(episode), true), - true - ); - // Add HDRezka source for episodes const hdrezkaEpisodePromise = processExternalSource('hdrezka', processHDRezkaSource('series', id, parseInt(season), parseInt(episode), true), true ); - const fetchPromises: Promise[] = [xprimeEpisodePromise, hdrezkaEpisodePromise]; + const fetchPromises: Promise[] = [hdrezkaEpisodePromise]; // Wait only for external promises now 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: string[] = ['xprime', 'hdrezka']; + const sourceTypes: string[] = ['hdrezka']; results.forEach((result, index) => { const source = sourceTypes[Math.min(index, sourceTypes.length - 1)]; console.log(`📊 [loadEpisodeStreams:${source}] Status: ${result.status}`); diff --git a/src/screens/InternalProvidersSettings.tsx b/src/screens/InternalProvidersSettings.tsx index 69845b8bd..996d3d164 100644 --- a/src/screens/InternalProvidersSettings.tsx +++ b/src/screens/InternalProvidersSettings.tsx @@ -104,21 +104,14 @@ const InternalProvidersSettings: React.FC = () => { const navigation = useNavigation(); // Individual provider states - const [xprimeEnabled, setXprimeEnabled] = useState(true); const [hdrezkaEnabled, setHdrezkaEnabled] = useState(true); // Load individual provider settings useEffect(() => { const loadProviderSettings = async () => { try { - const xprimeSettings = await AsyncStorage.getItem('xprime_settings'); const hdrezkaSettings = await AsyncStorage.getItem('hdrezka_settings'); - if (xprimeSettings) { - const parsed = JSON.parse(xprimeSettings); - setXprimeEnabled(parsed.enabled !== false); - } - if (hdrezkaSettings) { const parsed = JSON.parse(hdrezkaSettings); setHdrezkaEnabled(parsed.enabled !== false); @@ -139,7 +132,7 @@ const InternalProvidersSettings: React.FC = () => { if (!enabled) { Alert.alert( 'Disable Internal Providers', - 'This will disable all built-in streaming providers (XPRIME, HDRezka). You can still use external Stremio addons.', + 'This will disable all built-in streaming providers (HDRezka). You can still use external Stremio addons.', [ { text: 'Cancel', style: 'cancel' }, { @@ -156,15 +149,6 @@ const InternalProvidersSettings: React.FC = () => { } }, [updateSetting]); - const handleXprimeToggle = useCallback(async (enabled: boolean) => { - setXprimeEnabled(enabled); - try { - await AsyncStorage.setItem('xprime_settings', JSON.stringify({ enabled })); - } catch (error) { - console.error('Error saving XPRIME settings:', error); - } - }, []); - const handleHdrezkaToggle = useCallback(async (enabled: boolean) => { setHdrezkaEnabled(enabled); try { @@ -257,14 +241,6 @@ const InternalProvidersSettings: React.FC = () => { { backgroundColor: currentTheme.colors.elevation2 }, ]} > - { { id: 'all', name: 'All Providers' }, ...Array.from(allProviders) .sort((a, b) => { - // Always put XPRIME at the top (primary source) - if (a === 'xprime') return -1; - if (b === 'xprime') return 1; - - // Then put HDRezka second + // Put HDRezka first if (a === 'hdrezka') return -1; if (b === 'hdrezka') return 1; @@ -801,7 +797,6 @@ export const StreamsScreen = () => { } // Then try to match standalone quality numbers at the end of the title - // This handles XPRIME format where quality is just "1080", "720", etc. const matchAtEnd = title.match(/\b(\d{3,4})\s*$/); if (matchAtEnd) { const quality = parseInt(matchAtEnd[1], 10); @@ -844,11 +839,7 @@ export const StreamsScreen = () => { return addonId === selectedProvider; }) .sort(([addonIdA], [addonIdB]) => { - // Always put XPRIME at the top (primary source) - if (addonIdA === 'xprime') return -1; - if (addonIdB === 'xprime') return 1; - - // Then put HDRezka second + // Put HDRezka first if (addonIdA === 'hdrezka') return -1; if (addonIdB === 'hdrezka') return 1; @@ -869,13 +860,12 @@ export const StreamsScreen = () => { const qualityB = getQualityNumeric(b.title); return qualityB - qualityA; // Sort descending (e.g., 1080p before 720p) }); - } else if (addonId === 'xprime') { - // Sort XPRIME streams by quality in descending order (highest quality first) - // For XPRIME, quality is in the 'name' field + } else { + // Sort other streams by quality if possible sortedProviderStreams = [...providerStreams].sort((a, b) => { - const qualityA = getQualityNumeric(a.name); - const qualityB = getQualityNumeric(b.name); - return qualityB - qualityA; // Sort descending (e.g., 1080 before 720) + const qualityA = getQualityNumeric(a.name || a.title); + const qualityB = getQualityNumeric(b.name || b.title); + return qualityB - qualityA; // Sort descending }); } return { diff --git a/src/screens/TMDBSettingsScreen.tsx b/src/screens/TMDBSettingsScreen.tsx index 250399904..f65540552 100644 --- a/src/screens/TMDBSettingsScreen.tsx +++ b/src/screens/TMDBSettingsScreen.tsx @@ -115,13 +115,12 @@ const TMDBSettingsScreen = () => { const testApiKey = async (key: string): Promise => { try { - // Simple API call to test the key + // Simple API call to test the key using the API key parameter method const response = await fetch( - 'https://api.themoviedb.org/3/configuration', + `https://api.themoviedb.org/3/configuration?api_key=${key}`, { method: 'GET', headers: { - 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json', } } @@ -523,7 +522,7 @@ const TMDBSettingsScreen = () => { setApiKey(text); if (testResult) setTestResult(null); }} - placeholder="Paste your TMDb API key (v4 auth)" + placeholder="Paste your TMDb API key (v3)" placeholderTextColor={currentTheme.colors.mediumGray} autoCapitalize="none" autoCorrect={false} @@ -591,7 +590,7 @@ const TMDBSettingsScreen = () => { - To get your own TMDb API key (v4 auth token), you need to create a TMDb account and request an API key from their website. + To get your own TMDb API key (v3), you need to create a TMDb account and request an API key from their website. Using your own API key gives you dedicated quota and may improve app performance. diff --git a/src/services/xprimeService.ts b/src/services/xprimeService.ts index ab335834c..704982f38 100644 --- a/src/services/xprimeService.ts +++ b/src/services/xprimeService.ts @@ -197,91 +197,9 @@ class XprimeService { } async getStreams(mediaId: string, mediaType: string, season?: number, episode?: number): Promise { - try { - logger.log(`[XPRIME] Getting streams for ${mediaType} with ID: ${mediaId}`); - - // First check if internal providers are enabled - const settingsJson = await AsyncStorage.getItem('app_settings'); - if (settingsJson) { - const settings = JSON.parse(settingsJson); - if (settings.enableInternalProviders === false) { - logger.log('[XPRIME] Internal providers are disabled in settings, skipping Xprime.tv'); - return []; - } - } - - // Check individual XPRIME provider setting - const xprimeSettingsJson = await AsyncStorage.getItem('xprime_settings'); - if (xprimeSettingsJson) { - const xprimeSettings = JSON.parse(xprimeSettingsJson); - if (xprimeSettings.enabled === false) { - logger.log('[XPRIME] XPRIME provider is disabled in settings, skipping Xprime.tv'); - return []; - } - } - - // Extract the actual title from TMDB if this is an ID - let title = mediaId; - let year: number | undefined = undefined; - - if (mediaId.startsWith('tt') || mediaId.startsWith('tmdb:')) { - let tmdbId: number | null = null; - - // Handle IMDB IDs - if (mediaId.startsWith('tt')) { - logger.log(`[XPRIME] Converting IMDB ID to TMDB ID: ${mediaId}`); - tmdbId = await tmdbService.findTMDBIdByIMDB(mediaId); - } - // Handle TMDB IDs - else if (mediaId.startsWith('tmdb:')) { - tmdbId = parseInt(mediaId.split(':')[1], 10); - } - - if (tmdbId) { - // Fetch metadata from TMDB API - if (mediaType === 'movie') { - logger.log(`[XPRIME] Fetching movie details from TMDB for ID: ${tmdbId}`); - const movieDetails = await tmdbService.getMovieDetails(tmdbId.toString()); - if (movieDetails) { - title = movieDetails.title; - year = movieDetails.release_date ? parseInt(movieDetails.release_date.substring(0, 4), 10) : undefined; - logger.log(`[XPRIME] Using movie title "${title}" (${year}) for search`); - } - } else { - logger.log(`[XPRIME] Fetching TV show details from TMDB for ID: ${tmdbId}`); - const showDetails = await tmdbService.getTVShowDetails(tmdbId); - if (showDetails) { - title = showDetails.name; - year = showDetails.first_air_date ? parseInt(showDetails.first_air_date.substring(0, 4), 10) : undefined; - logger.log(`[XPRIME] Using TV show title "${title}" (${year}) for search`); - } - } - } - } - - if (!title || !year) { - logger.log('[XPRIME] Skipping fetch: title or year is missing.'); - return []; - } - - const rawXprimeStreams = await this.getXprimeStreams(title, year, mediaType, season, episode); - - // Convert to Stream format - const streams: Stream[] = rawXprimeStreams.map(xprimeStream => ({ - name: `XPRIME ${xprimeStream.quality.toUpperCase()}`, - title: xprimeStream.size !== 'Unknown size' ? xprimeStream.size : '', - url: xprimeStream.url, - behaviorHints: { - notWebReady: false - } - })); - - logger.log(`[XPRIME] Found ${streams.length} streams`); - return streams; - } catch (error) { - logger.error(`[XPRIME] Error getting streams:`, error); - return []; - } + // XPRIME service has been removed from internal providers + logger.log('[XPRIME] Service has been removed from internal providers'); + return []; } private async getXprimeStreams(title: string, year: number, type: string, seasonNum?: number, episodeNum?: number): Promise {