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
*/
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 (
<PaperProvider theme={customDarkTheme}>
@ -62,7 +69,8 @@ const ThemedApp = () => {
<StatusBar
style="light"
/>
<AppNavigator />
{!isAppReady && <SplashScreen onFinish={handleSplashComplete} />}
{isAppReady && <AppNavigator />}
</View>
</NavigationContainer>
</PaperProvider>

View file

@ -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"

View file

@ -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"
}
}
},

View file

@ -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;
})();
module.exports = config;

67
package-lock.json generated
View file

@ -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"

View file

@ -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",

View file

@ -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');

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) => {
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}
/>
</TouchableOpacity>
</View>

View file

@ -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<StreamingContent[]>([]);
const [loadingRecommendations, setLoadingRecommendations] = useState(false);
const [imdbId, setImdbId] = useState<string | null>(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<any>, 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<any>[] = [xprimePromise, hdrezkaPromise];
// Include HDRezka in fetchPromises array
const fetchPromises: Promise<any>[] = [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<any>[] = [xprimeEpisodePromise, hdrezkaEpisodePromise];
const fetchPromises: Promise<any>[] = [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}`);

View file

@ -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 },
]}
>
<SettingItem
title="XPRIME"
description="High-quality streams with various resolutions"
icon="star"
value={xprimeEnabled}
onValueChange={handleXprimeToggle}
badge="NEW"
/>
<SettingItem
title="HDRezka"
description="Popular streaming service with multiple quality options"

View file

@ -749,11 +749,7 @@ export const StreamsScreen = () => {
{ 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 {

View file

@ -115,13 +115,12 @@ const TMDBSettingsScreen = () => {
const testApiKey = async (key: string): Promise<boolean> => {
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 = () => {
<View style={styles.infoCard}>
<MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} />
<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.
</Text>
</View>

View file

@ -197,91 +197,9 @@ class XprimeService {
}
async getStreams(mediaId: string, mediaType: string, season?: number, episode?: number): Promise<Stream[]> {
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<XprimeStream[]> {