From 9e03619db77a84c6697f78a3f604152a8630ecd2 Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 18 Jun 2025 09:02:48 +0530 Subject: [PATCH] Refactor internal provider settings and enhance streaming experience This update removes the XPRIME provider from internal settings, streamlining the provider management process. The HDRezka provider is now prioritized in the UI, improving user experience. Additionally, various components have been optimized for better performance, including enhancements to the VideoPlayer for improved buffering and seeking behavior on Android devices. The app's theme has been updated to a dark mode, and several dependencies have been upgraded for better stability and performance. --- App.tsx | 12 +- app.json | 11 +- eas.json | 10 +- metro.config.js | 47 +++---- package-lock.json | 67 ++++++---- package.json | 2 +- scripts/test-hdrezka.js | 2 +- src/components/SplashScreen.tsx | 59 +++++++++ src/components/player/VideoPlayer.tsx | 147 +++++++++++++++++----- src/hooks/useMetadata.ts | 108 +++++----------- src/screens/InternalProvidersSettings.tsx | 26 +--- src/screens/StreamsScreen.tsx | 24 ++-- src/screens/TMDBSettingsScreen.tsx | 9 +- src/services/xprimeService.ts | 88 +------------ 14 files changed, 321 insertions(+), 291 deletions(-) create mode 100644 src/components/SplashScreen.tsx diff --git a/App.tsx b/App.tsx index df02cd8..bf19654 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 07c328d..453ab6c 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 afd500a..b208a76 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 6218690..79fe23f 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 60caca1..a8a42dd 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 7d70e09..2fef212 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 d1e8ab1..1d190aa 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 0000000..fabe7b8 --- /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 3da4a29..2655caa 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 c6941c1..fe71136 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 69845b8..996d3d1 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 2503999..f655405 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 ab33583..704982f 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 {