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.
This commit is contained in:
tapframe 2025-06-18 09:02:48 +05:30
parent 046c9e3f97
commit 9e03619db7
14 changed files with 321 additions and 291 deletions

12
App.tsx
View file

@ -5,7 +5,7 @@
* @format * @format
*/ */
import React from 'react'; import React, { useState } from 'react';
import { import {
View, View,
StyleSheet StyleSheet
@ -24,6 +24,7 @@ import { CatalogProvider } from './src/contexts/CatalogContext';
import { GenreProvider } from './src/contexts/GenreContext'; import { GenreProvider } from './src/contexts/GenreContext';
import { TraktProvider } from './src/contexts/TraktContext'; import { TraktProvider } from './src/contexts/TraktContext';
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext'; import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
import SplashScreen from './src/components/SplashScreen';
// This fixes many navigation layout issues by using native screen containers // This fixes many navigation layout issues by using native screen containers
enableScreens(true); enableScreens(true);
@ -31,6 +32,7 @@ enableScreens(true);
// Inner app component that uses the theme context // Inner app component that uses the theme context
const ThemedApp = () => { const ThemedApp = () => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const [isAppReady, setIsAppReady] = useState(false);
// Create custom themes based on current theme // Create custom themes based on current theme
const customDarkTheme = { const customDarkTheme = {
@ -50,6 +52,11 @@ const ThemedApp = () => {
background: currentTheme.colors.darkBackground, background: currentTheme.colors.darkBackground,
} }
}; };
// Handler for splash screen completion
const handleSplashComplete = () => {
setIsAppReady(true);
};
return ( return (
<PaperProvider theme={customDarkTheme}> <PaperProvider theme={customDarkTheme}>
@ -62,7 +69,8 @@ const ThemedApp = () => {
<StatusBar <StatusBar
style="light" style="light"
/> />
<AppNavigator /> {!isAppReady && <SplashScreen onFinish={handleSplashComplete} />}
{isAppReady && <AppNavigator />}
</View> </View>
</NavigationContainer> </NavigationContainer>
</PaperProvider> </PaperProvider>

View file

@ -5,13 +5,13 @@
"version": "1.0.0", "version": "1.0.0",
"orientation": "default", "orientation": "default",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"userInterfaceStyle": "light", "userInterfaceStyle": "dark",
"scheme": "stremioexpo", "scheme": "stremioexpo",
"newArchEnabled": true, "newArchEnabled": true,
"splash": { "splash": {
"image": "./assets/splash-icon.png", "image": "./assets/splash-icon.png",
"resizeMode": "contain", "resizeMode": "contain",
"backgroundColor": "#ffffff" "backgroundColor": "#020404"
}, },
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": true,
@ -49,7 +49,12 @@
"WAKE_LOCK" "WAKE_LOCK"
], ],
"package": "com.nuvio.app", "package": "com.nuvio.app",
"enableSplitAPKs": true "enableSplitAPKs": true,
"versionCode": 1,
"enableProguardInReleaseBuilds": true,
"enableHermes": true,
"enableSeparateBuildPerCPUArchitecture": true,
"enableVectorDrawables": true
}, },
"web": { "web": {
"favicon": "./assets/favicon.png" "favicon": "./assets/favicon.png"

View file

@ -13,7 +13,12 @@
}, },
"production": { "production": {
"autoIncrement": true, "autoIncrement": true,
"extends": "apk" "extends": "apk",
"android": {
"buildType": "apk",
"gradleCommand": ":app:assembleRelease",
"image": "latest"
}
}, },
"release": { "release": {
"distribution": "store", "distribution": "store",
@ -23,7 +28,8 @@
}, },
"apk": { "apk": {
"android": { "android": {
"buildType": "apk" "buildType": "apk",
"gradleCommand": ":app:assembleRelease"
} }
} }
}, },

View file

@ -1,28 +1,31 @@
const { getDefaultConfig } = require('expo/metro-config'); const { getDefaultConfig } = require('expo/metro-config');
module.exports = (() => { const config = getDefaultConfig(__dirname);
const config = getDefaultConfig(__dirname);
const { transformer, resolver } = config; // Enable tree shaking and better minification
config.transformer = {
config.transformer = { ...config.transformer,
...transformer, babelTransformerPath: require.resolve('react-native-svg-transformer'),
babelTransformerPath: require.resolve('react-native-svg-transformer'), minifierConfig: {
minifierConfig: { ecma: 8,
compress: { keep_fnames: true,
// Remove console.* statements in release builds mangle: {
drop_console: true, keep_fnames: true,
// Keep error logging for critical issues
pure_funcs: ['console.info', 'console.log', 'console.debug', 'console.warn'],
},
}, },
}; compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info', 'console.debug'],
},
},
};
config.resolver = { // Optimize resolver for better tree shaking and SVG support
...resolver, config.resolver = {
assetExts: resolver.assetExts.filter((ext) => ext !== 'svg'), ...config.resolver,
sourceExts: [...resolver.sourceExts, 'svg'], assetExts: config.resolver.assetExts.filter((ext) => ext !== 'svg'),
}; sourceExts: [...config.resolver.sourceExts, 'svg'],
resolverMainFields: ['react-native', 'browser', 'main'],
};
return config; module.exports = config;
})();

67
package-lock.json generated
View file

@ -24,7 +24,7 @@
"@shopify/flash-list": "1.7.3", "@shopify/flash-list": "1.7.3",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20", "@types/react-native-video": "^5.0.20",
"axios": "^1.8.4", "axios": "^1.10.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
@ -4927,9 +4927,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.8.4", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
@ -5554,21 +5554,21 @@
} }
}, },
"node_modules/cheerio": { "node_modules/cheerio": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.0.tgz",
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", "integrity": "sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cheerio-select": "^2.1.0", "cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0", "dom-serializer": "^2.0.0",
"domhandler": "^5.0.3", "domhandler": "^5.0.3",
"domutils": "^3.1.0", "domutils": "^3.2.2",
"encoding-sniffer": "^0.2.0", "encoding-sniffer": "^0.2.0",
"htmlparser2": "^9.1.0", "htmlparser2": "^10.0.0",
"parse5": "^7.1.2", "parse5": "^7.3.0",
"parse5-htmlparser2-tree-adapter": "^7.0.0", "parse5-htmlparser2-tree-adapter": "^7.1.0",
"parse5-parser-stream": "^7.1.2", "parse5-parser-stream": "^7.1.2",
"undici": "^6.19.5", "undici": "^7.10.0",
"whatwg-mimetype": "^4.0.0" "whatwg-mimetype": "^4.0.0"
}, },
"engines": { "engines": {
@ -5595,6 +5595,15 @@
"url": "https://github.com/sponsors/fb55" "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": { "node_modules/chownr": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@ -6143,9 +6152,9 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@ -7880,9 +7889,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "9.1.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
"funding": [ "funding": [
"https://github.com/fb55/htmlparser2?sponsor=1", "https://github.com/fb55/htmlparser2?sponsor=1",
{ {
@ -7894,8 +7903,20 @@
"dependencies": { "dependencies": {
"domelementtype": "^2.3.0", "domelementtype": "^2.3.0",
"domhandler": "^5.0.3", "domhandler": "^5.0.3",
"domutils": "^3.1.0", "domutils": "^3.2.1",
"entities": "^4.5.0" "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": { "node_modules/http-errors": {
@ -13696,9 +13717,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.18.1", "version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View file

@ -25,7 +25,7 @@
"@shopify/flash-list": "1.7.3", "@shopify/flash-list": "1.7.3",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20", "@types/react-native-video": "^5.0.20",
"axios": "^1.8.4", "axios": "^1.10.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",

View file

@ -1,4 +1,4 @@
// Test script for HDRezka service d// Test script for HDRezka service
// Run with: node scripts/test-hdrezka.js // Run with: node scripts/test-hdrezka.js
const fetch = require('node-fetch'); const fetch = require('node-fetch');

View file

@ -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 (
<Animated.View style={[styles.container, { opacity: fadeAnim }]}>
<Image
source={require('../../assets/splash-icon.png')}
style={styles.image}
resizeMode="contain"
/>
</Animated.View>
);
};
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;

View file

@ -297,16 +297,56 @@ const VideoPlayer: React.FC = () => {
const seekToTime = (timeInSeconds: number) => { const seekToTime = (timeInSeconds: number) => {
if (!isPlayerReady || duration <= 0 || !vlcRef.current) return; if (!isPlayerReady || duration <= 0 || !vlcRef.current) return;
const normalizedPosition = Math.max(0, Math.min(timeInSeconds / duration, 1)); const normalizedPosition = Math.max(0, Math.min(timeInSeconds / duration, 1));
try { 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); vlcRef.current.setPosition(normalizedPosition);
} else if (typeof vlcRef.current.seek === 'function') { lastSeekTime.current = now;
vlcRef.current.seek(normalizedPosition);
// Reset buffering state after a delay
setTimeout(() => {
setIsBuffering(false);
}, 500);
} else { } 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) { } catch (error) {
logger.error('[VideoPlayer] Error during seek operation:', error); logger.error('[VideoPlayer] Error during seek operation:', error);
setIsBuffering(false);
} }
}; };
@ -329,8 +369,18 @@ const VideoPlayer: React.FC = () => {
const handleProgressBarDragEnd = () => { const handleProgressBarDragEnd = () => {
setIsDragging(false); setIsDragging(false);
if (pendingSeekValue.current !== null) { if (pendingSeekValue.current !== null) {
seekToTime(pendingSeekValue.current); // For Android, add a small delay to ensure UI updates before the seek happens
pendingSeekValue.current = null; 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`); 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) { 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(() => { setTimeout(() => {
if (vlcRef.current && duration > 0 && pendingSeek) { if (vlcRef.current && duration > 0 && pendingSeek) {
logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`); logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`);
// Use our existing seekToTime function which handles VLC methods properly if (Platform.OS === 'android') {
seekToTime(pendingSeek.position); // On Android, wait longer and set isBuffering to improve visual feedback
setIsBuffering(true);
// Also update the current time state to reflect the seek
setCurrentTime(pendingSeek.position); // For Android, use setPosition directly with normalized value
const normalizedPosition = Math.max(0, Math.min(pendingSeek.position / duration, 1));
// Resume playback if it was playing before the source change vlcRef.current.setPosition(normalizedPosition);
if (pendingSeek.shouldPlay) {
// Update the current time
setCurrentTime(pendingSeek.position);
// Give the player time to recover from the seek
setTimeout(() => { setTimeout(() => {
logger.log('[VideoPlayer] Resuming playback after seek'); setIsBuffering(false);
setPaused(false);
if (vlcRef.current && typeof vlcRef.current.play === 'function') { // Resume playback after a delay if needed
vlcRef.current.play(); 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 { } else {
// No seeking needed, just resume playback if it was playing // No seeking needed, just resume playback if it was playing
if (pendingSeek.shouldPlay) { if (pendingSeek.shouldPlay) {
@ -963,7 +1041,19 @@ const VideoPlayer: React.FC = () => {
}} }}
source={{ source={{
uri: currentStreamUrl, 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', '--rtsp-tcp',
'--network-caching=150', '--network-caching=150',
'--rtsp-caching=150', '--rtsp-caching=150',
@ -984,6 +1074,7 @@ const VideoPlayer: React.FC = () => {
onProgress={handleProgress} onProgress={handleProgress}
onEnd={onEnd} onEnd={onEnd}
onError={handleError} onError={handleError}
onBuffering={onBuffering}
/> />
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View file

@ -4,12 +4,13 @@ import { catalogService } from '../services/catalogService';
import { stremioService } from '../services/stremioService'; import { stremioService } from '../services/stremioService';
import { tmdbService } from '../services/tmdbService'; import { tmdbService } from '../services/tmdbService';
import { hdrezkaService } from '../services/hdrezkaService'; import { hdrezkaService } from '../services/hdrezkaService';
import { xprimeService } from '../services/xprimeService';
import { cacheService } from '../services/cacheService'; import { cacheService } from '../services/cacheService';
import { Cast, Episode, GroupedEpisodes, GroupedStreams } from '../types/metadata'; import { Cast, Episode, GroupedEpisodes, GroupedStreams } from '../types/metadata';
import { TMDBService } from '../services/tmdbService'; import { TMDBService } from '../services/tmdbService';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { usePersistentSeasons } from './usePersistentSeasons'; import { usePersistentSeasons } from './usePersistentSeasons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Stream } from '../types/metadata';
// Constants for timeouts and retries // Constants for timeouts and retries
const API_TIMEOUT = 10000; // 10 seconds const API_TIMEOUT = 10000; // 10 seconds
@ -115,6 +116,8 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
const [recommendations, setRecommendations] = useState<StreamingContent[]>([]); const [recommendations, setRecommendations] = useState<StreamingContent[]>([]);
const [loadingRecommendations, setLoadingRecommendations] = useState(false); const [loadingRecommendations, setLoadingRecommendations] = useState(false);
const [imdbId, setImdbId] = useState<string | null>(null); const [imdbId, setImdbId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [availableStreams, setAvailableStreams] = useState<{ [sourceType: string]: Stream }>({});
// Add hook for persistent seasons // Add hook for persistent seasons
const { getSeason, saveSeason } = usePersistentSeasons(); 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<any>, isEpisode = false) => { const processExternalSource = async (sourceType: string, promise: Promise<any>, isEpisode = false) => {
const sourceStartTime = Date.now();
const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams';
try { try {
logger.log(`🔍 [${logPrefix}:${sourceType}] Starting fetch`); const startTime = Date.now();
const result = await promise; 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) { if (result && Object.keys(result).length > 0) {
const totalStreams = Object.values(result).reduce((acc, group: any) => acc + (group.streams?.length || 0), 0); // Update the appropriate state based on whether this is for an episode or not
logger.log(`📦 [${logPrefix}:${sourceType}] Found ${totalStreams} streams`);
const updateState = (prevState: GroupedStreams) => { 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 // Merge in the new streams
if (sourceType === 'xprime') { Object.entries(result).forEach(([provider, data]: [string, any]) => {
return { ...result, ...prevState }; newState[provider] = data;
} else { });
return { ...prevState, ...result };
} return newState;
}; };
if (isEpisode) { if (isEpisode) {
setEpisodeStreams(updateState); setEpisodeStreams(updateState);
} else { } else {
setGroupedStreams(updateState); 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 { } else {
logger.log(`⚠️ [${logPrefix}:${sourceType}] No streams found`); console.log(`⚠️ [processExternalSource:${sourceType}] No streams found after ${processingTime}ms`);
return {};
} }
return result;
} catch (error) { } catch (error) {
logger.error(`❌ [${logPrefix}:${sourceType}] Error:`, error); console.error(`❌ [processExternalSource:${sourceType}] Error:`, error);
return {}; return {};
} }
}; };
@ -661,9 +629,6 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
console.log('🚀 [loadStreams] START - Loading streams for:', id); console.log('🚀 [loadStreams] START - Loading streams for:', id);
updateLoadingState(); 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 // Get TMDB ID for external sources first before starting parallel requests
console.log('🔍 [loadStreams] Getting TMDB ID for:', id); console.log('🔍 [loadStreams] Getting TMDB ID for:', id);
let tmdbId; let tmdbId;
@ -679,27 +644,22 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
tmdbId = id; tmdbId = id;
console.log(' [loadStreams] Using ID as TMDB ID:', tmdbId); console.log(' [loadStreams] Using ID as TMDB ID:', tmdbId);
} }
console.log('🔄 [loadStreams] Starting stream requests');
// Start Stremio request using the callback method // Start Stremio request using the callback method
processStremioSource(type, id, false); processStremioSource(type, id, false);
// Add Xprime source (PRIMARY)
const xprimePromise = processExternalSource('xprime', processXprimeSource(type, id), false);
// Add HDRezka source // Add HDRezka source
const hdrezkaPromise = processExternalSource('hdrezka', processHDRezkaSource(type, id), false); const hdrezkaPromise = processExternalSource('hdrezka', processHDRezkaSource(type, id), false);
// Include Xprime and HDRezka in fetchPromises array (Xprime first) // Include HDRezka in fetchPromises array
const fetchPromises: Promise<any>[] = [xprimePromise, hdrezkaPromise]; const fetchPromises: Promise<any>[] = [hdrezkaPromise];
// Wait only for external promises now // Wait only for external promises now
const results = await Promise.allSettled(fetchPromises); const results = await Promise.allSettled(fetchPromises);
const totalTime = Date.now() - startTime; const totalTime = Date.now() - startTime;
console.log(`✅ [loadStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`); 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) => { results.forEach((result, index) => {
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)]; const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
console.log(`📊 [loadStreams:${source}] Status: ${result.status}`); 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 // Start Stremio request using the callback method
processStremioSource('series', episodeId, true); 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 // Add HDRezka source for episodes
const hdrezkaEpisodePromise = processExternalSource('hdrezka', const hdrezkaEpisodePromise = processExternalSource('hdrezka',
processHDRezkaSource('series', id, parseInt(season), parseInt(episode), true), processHDRezkaSource('series', id, parseInt(season), parseInt(episode), true),
true true
); );
const fetchPromises: Promise<any>[] = [xprimeEpisodePromise, hdrezkaEpisodePromise]; const fetchPromises: Promise<any>[] = [hdrezkaEpisodePromise];
// Wait only for external promises now // Wait only for external promises now
const results = await Promise.allSettled(fetchPromises); const results = await Promise.allSettled(fetchPromises);
const totalTime = Date.now() - startTime; const totalTime = Date.now() - startTime;
console.log(`✅ [loadEpisodeStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`); 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) => { results.forEach((result, index) => {
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)]; const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
console.log(`📊 [loadEpisodeStreams:${source}] Status: ${result.status}`); console.log(`📊 [loadEpisodeStreams:${source}] Status: ${result.status}`);

View file

@ -104,21 +104,14 @@ const InternalProvidersSettings: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
// Individual provider states // Individual provider states
const [xprimeEnabled, setXprimeEnabled] = useState(true);
const [hdrezkaEnabled, setHdrezkaEnabled] = useState(true); const [hdrezkaEnabled, setHdrezkaEnabled] = useState(true);
// Load individual provider settings // Load individual provider settings
useEffect(() => { useEffect(() => {
const loadProviderSettings = async () => { const loadProviderSettings = async () => {
try { try {
const xprimeSettings = await AsyncStorage.getItem('xprime_settings');
const hdrezkaSettings = await AsyncStorage.getItem('hdrezka_settings'); const hdrezkaSettings = await AsyncStorage.getItem('hdrezka_settings');
if (xprimeSettings) {
const parsed = JSON.parse(xprimeSettings);
setXprimeEnabled(parsed.enabled !== false);
}
if (hdrezkaSettings) { if (hdrezkaSettings) {
const parsed = JSON.parse(hdrezkaSettings); const parsed = JSON.parse(hdrezkaSettings);
setHdrezkaEnabled(parsed.enabled !== false); setHdrezkaEnabled(parsed.enabled !== false);
@ -139,7 +132,7 @@ const InternalProvidersSettings: React.FC = () => {
if (!enabled) { if (!enabled) {
Alert.alert( Alert.alert(
'Disable Internal Providers', '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' }, { text: 'Cancel', style: 'cancel' },
{ {
@ -156,15 +149,6 @@ const InternalProvidersSettings: React.FC = () => {
} }
}, [updateSetting]); }, [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) => { const handleHdrezkaToggle = useCallback(async (enabled: boolean) => {
setHdrezkaEnabled(enabled); setHdrezkaEnabled(enabled);
try { try {
@ -257,14 +241,6 @@ const InternalProvidersSettings: React.FC = () => {
{ backgroundColor: currentTheme.colors.elevation2 }, { backgroundColor: currentTheme.colors.elevation2 },
]} ]}
> >
<SettingItem
title="XPRIME"
description="High-quality streams with various resolutions"
icon="star"
value={xprimeEnabled}
onValueChange={handleXprimeToggle}
badge="NEW"
/>
<SettingItem <SettingItem
title="HDRezka" title="HDRezka"
description="Popular streaming service with multiple quality options" description="Popular streaming service with multiple quality options"

View file

@ -749,11 +749,7 @@ export const StreamsScreen = () => {
{ id: 'all', name: 'All Providers' }, { id: 'all', name: 'All Providers' },
...Array.from(allProviders) ...Array.from(allProviders)
.sort((a, b) => { .sort((a, b) => {
// Always put XPRIME at the top (primary source) // Put HDRezka first
if (a === 'xprime') return -1;
if (b === 'xprime') return 1;
// Then put HDRezka second
if (a === 'hdrezka') return -1; if (a === 'hdrezka') return -1;
if (b === '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 // 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*$/); const matchAtEnd = title.match(/\b(\d{3,4})\s*$/);
if (matchAtEnd) { if (matchAtEnd) {
const quality = parseInt(matchAtEnd[1], 10); const quality = parseInt(matchAtEnd[1], 10);
@ -844,11 +839,7 @@ export const StreamsScreen = () => {
return addonId === selectedProvider; return addonId === selectedProvider;
}) })
.sort(([addonIdA], [addonIdB]) => { .sort(([addonIdA], [addonIdB]) => {
// Always put XPRIME at the top (primary source) // Put HDRezka first
if (addonIdA === 'xprime') return -1;
if (addonIdB === 'xprime') return 1;
// Then put HDRezka second
if (addonIdA === 'hdrezka') return -1; if (addonIdA === 'hdrezka') return -1;
if (addonIdB === 'hdrezka') return 1; if (addonIdB === 'hdrezka') return 1;
@ -869,13 +860,12 @@ export const StreamsScreen = () => {
const qualityB = getQualityNumeric(b.title); const qualityB = getQualityNumeric(b.title);
return qualityB - qualityA; // Sort descending (e.g., 1080p before 720p) return qualityB - qualityA; // Sort descending (e.g., 1080p before 720p)
}); });
} else if (addonId === 'xprime') { } else {
// Sort XPRIME streams by quality in descending order (highest quality first) // Sort other streams by quality if possible
// For XPRIME, quality is in the 'name' field
sortedProviderStreams = [...providerStreams].sort((a, b) => { sortedProviderStreams = [...providerStreams].sort((a, b) => {
const qualityA = getQualityNumeric(a.name); const qualityA = getQualityNumeric(a.name || a.title);
const qualityB = getQualityNumeric(b.name); const qualityB = getQualityNumeric(b.name || b.title);
return qualityB - qualityA; // Sort descending (e.g., 1080 before 720) return qualityB - qualityA; // Sort descending
}); });
} }
return { return {

View file

@ -115,13 +115,12 @@ const TMDBSettingsScreen = () => {
const testApiKey = async (key: string): Promise<boolean> => { const testApiKey = async (key: string): Promise<boolean> => {
try { 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( const response = await fetch(
'https://api.themoviedb.org/3/configuration', `https://api.themoviedb.org/3/configuration?api_key=${key}`,
{ {
method: 'GET', method: 'GET',
headers: { headers: {
'Authorization': `Bearer ${key}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
} }
@ -523,7 +522,7 @@ const TMDBSettingsScreen = () => {
setApiKey(text); setApiKey(text);
if (testResult) setTestResult(null); if (testResult) setTestResult(null);
}} }}
placeholder="Paste your TMDb API key (v4 auth)" placeholder="Paste your TMDb API key (v3)"
placeholderTextColor={currentTheme.colors.mediumGray} placeholderTextColor={currentTheme.colors.mediumGray}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
@ -591,7 +590,7 @@ const TMDBSettingsScreen = () => {
<View style={styles.infoCard}> <View style={styles.infoCard}>
<MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} /> <MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} />
<Text style={styles.infoText}> <Text style={styles.infoText}>
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. Using your own API key gives you dedicated quota and may improve app performance.
</Text> </Text>
</View> </View>

View file

@ -197,91 +197,9 @@ class XprimeService {
} }
async getStreams(mediaId: string, mediaType: string, season?: number, episode?: number): Promise<Stream[]> { async getStreams(mediaId: string, mediaType: string, season?: number, episode?: number): Promise<Stream[]> {
try { // XPRIME service has been removed from internal providers
logger.log(`[XPRIME] Getting streams for ${mediaType} with ID: ${mediaId}`); logger.log('[XPRIME] Service has been removed from internal providers');
return [];
// 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 [];
}
} }
private async getXprimeStreams(title: string, year: number, type: string, seasonNum?: number, episodeNum?: number): Promise<XprimeStream[]> { private async getXprimeStreams(title: string, year: number, type: string, seasonNum?: number, episodeNum?: number): Promise<XprimeStream[]> {