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