mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
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:
parent
046c9e3f97
commit
9e03619db7
14 changed files with 321 additions and 291 deletions
12
App.tsx
12
App.tsx
|
|
@ -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>
|
||||||
|
|
|
||||||
11
app.json
11
app.json
|
|
@ -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"
|
||||||
|
|
|
||||||
10
eas.json
10
eas.json
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
67
package-lock.json
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
59
src/components/SplashScreen.tsx
Normal file
59
src/components/SplashScreen.tsx
Normal 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;
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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[]> {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue