some changes
11
.expo-shared/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
> Why do I have a folder named ".expo-shared" in my project?
|
||||||
|
|
||||||
|
The ".expo-shared" folder is created when running commands that produce state that is intended to be shared with all developers on the project. For example, "npx expo-optimize".
|
||||||
|
|
||||||
|
> What does the "assets.json" file contain?
|
||||||
|
|
||||||
|
The "assets.json" file describes the assets that have been optimized through "expo-optimize" and do not need to be processed again.
|
||||||
|
|
||||||
|
> Should I commit the ".expo-shared" folder?
|
||||||
|
|
||||||
|
Yes, you should share the ".expo-shared" folder with your collaborators.
|
||||||
19
.expo-shared/assets.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"1377cee8d8f2cab2c4146574910c2b66b1a92365bd9dc945f9240c6727aee0be": true,
|
||||||
|
"015a72aeb24c1166243ebc185355c9488cc892fbf3aa82f9ae66819a10f3451c": true,
|
||||||
|
"7d8138ee6378ca981d3b887798bd6f3d101a24012f58b96ba616ff7f4b179cd2": true,
|
||||||
|
"f14426796829173c18cb16bbb7839d370726835bcd16bf8a1813a0c418a4cf2e": true,
|
||||||
|
"b60b46556bf2a07fb4f1e14feb1728f2690c6d8e337ecd7b78f1cdea7ccbe2bf": true,
|
||||||
|
"107386eaf4e7f0d9c8256b3ff62011ec9dff059bb8df975bf8dae66729127606": true,
|
||||||
|
"5f4c0a732b6325bf4071d9124d2ae67e037cb24fcc9c482ef82bea742109a3b8": true,
|
||||||
|
"1115b80ca5da0e724a39b7f789771a42959d47aab327f36a2710fad69200857d": true,
|
||||||
|
"f543ece2dea5881d69abf9eb661da82deafb4b6ea8e48a73b628db0a88750816": true,
|
||||||
|
"30aa92b9e2028ff6ae19bcc91693d0d3d65d459c797ae882844472fa2f810cfa": true,
|
||||||
|
"77294e294a44cc6a0c5ac6d55b50733697be46cfb09322d1d84866ebc3272bed": true,
|
||||||
|
"24272cdaeff82cc5facdaccd982a6f05b60c4504704bbf94c19a6388659880bb": true,
|
||||||
|
"8b8c429e5130b53fa2caf330b76dfbf66e25109acb76f5f3f34325f25bb70274": true,
|
||||||
|
"7fc3cf4d858d741cb3078f63a8b3cb20951948e68c08309740f438f90b8480aa": true,
|
||||||
|
"d5997fd2d5ea21b27c3d124f04f79709744936ee083972aa868ddb6db4621d6f": true,
|
||||||
|
"81955b3156f2a65ec124956520ba74c6dd686d4a8a7cc06c1f6a415495d32314": true,
|
||||||
|
"71633e3d0bb9fbdb4956fd33aafdd4bfdb4cddc3f5f820f72b41b406fb23afdb": true
|
||||||
|
}
|
||||||
|
|
@ -86,9 +86,19 @@ android {
|
||||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
|
|
||||||
namespace 'com.stremio.expo'
|
namespace 'com.nuvio.app'
|
||||||
|
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
enable true
|
||||||
|
reset()
|
||||||
|
include "armeabi-v7a", "arm64-v8a"
|
||||||
|
universalApk false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'com.stremio.expo'
|
applicationId 'com.nuvio.app'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
|
|
@ -110,8 +120,8 @@ android {
|
||||||
// Caution! In production, you need to generate your own keystore file.
|
// Caution! In production, you need to generate your own keystore file.
|
||||||
// see https://reactnative.dev/docs/signed-apk-android.
|
// see https://reactnative.dev/docs/signed-apk-android.
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
|
shrinkResources true
|
||||||
minifyEnabled enableProguardInReleaseBuilds
|
minifyEnabled true
|
||||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
|
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
|
@ -18,7 +16,7 @@
|
||||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true">
|
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
|
@ -27,7 +25,7 @@
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<data android:scheme="com.stremio.expo"/>
|
<data android:scheme="com.nuvio.app"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
package com.stremio.expo
|
package com.nuvio.app
|
||||||
import expo.modules.splashscreen.SplashScreenManager
|
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -16,10 +15,7 @@ class MainActivity : ReactActivity() {
|
||||||
// Set the theme to AppTheme BEFORE onCreate to support
|
// Set the theme to AppTheme BEFORE onCreate to support
|
||||||
// coloring the background, status bar, and navigation bar.
|
// coloring the background, status bar, and navigation bar.
|
||||||
// This is required for expo-splash-screen.
|
// This is required for expo-splash-screen.
|
||||||
// setTheme(R.style.AppTheme);
|
setTheme(R.style.AppTheme);
|
||||||
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
|
|
||||||
SplashScreenManager.registerOnActivity(this)
|
|
||||||
// @generated end expo-splashscreen
|
|
||||||
super.onCreate(null)
|
super.onCreate(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.stremio.expo
|
package com.nuvio.app
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">stremio-expo</string>
|
<string name="app_name">Nuvio</string>
|
||||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||||
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
|
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,7 @@
|
||||||
<item name="android:textColorHint">#c8c8c8</item>
|
<item name="android:textColorHint">#c8c8c8</item>
|
||||||
<item name="android:textColor">@android:color/black</item>
|
<item name="android:textColor">@android:color/black</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
|
||||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
|
|
||||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -21,7 +21,7 @@ extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = 'stremio-expo'
|
rootProject.name = 'Nuvio'
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
versionCatalogs {
|
versionCatalogs {
|
||||||
|
|
|
||||||
6
app.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "stremio-expo",
|
"name": "Nuvio",
|
||||||
"slug": "stremio-expo",
|
"slug": "nuvio",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
"INTERNET",
|
"INTERNET",
|
||||||
"WAKE_LOCK"
|
"WAKE_LOCK"
|
||||||
],
|
],
|
||||||
"package": "com.stremio.expo"
|
"package": "com.nuvio.app"
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.6 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 720 B |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 56 KiB |
|
|
@ -5,5 +5,10 @@ module.exports = function (api) {
|
||||||
plugins: [
|
plugins: [
|
||||||
'react-native-reanimated/plugin',
|
'react-native-reanimated/plugin',
|
||||||
],
|
],
|
||||||
|
env: {
|
||||||
|
production: {
|
||||||
|
plugins: ['transform-remove-console'],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
5
eas.json
|
|
@ -19,6 +19,11 @@
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "app-bundle"
|
"buildType": "app-bundle"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"apk": {
|
||||||
|
"android": {
|
||||||
|
"buildType": "apk"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"submit": {
|
"submit": {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@ module.exports = (() => {
|
||||||
config.transformer = {
|
config.transformer = {
|
||||||
...transformer,
|
...transformer,
|
||||||
babelTransformerPath: require.resolve('react-native-svg-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'],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
config.resolver = {
|
config.resolver = {
|
||||||
|
|
|
||||||
84
package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "stremio-expo",
|
"name": "nuvio",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "stremio-expo",
|
"name": "nuvio",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^14.1.0",
|
"@expo/vector-icons": "^14.1.0",
|
||||||
|
|
@ -21,21 +21,16 @@
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"expo": "~52.0.43",
|
"expo": "~52.0.43",
|
||||||
"expo-av": "^15.0.2",
|
|
||||||
"expo-blur": "^14.0.3",
|
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
"expo-linear-gradient": "~14.0.2",
|
"expo-linear-gradient": "~14.0.2",
|
||||||
"expo-notifications": "~0.29.14",
|
"expo-notifications": "~0.29.14",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
"expo-sharing": "^13.0.1",
|
|
||||||
"expo-splash-screen": "^0.29.22",
|
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
"expo-system-ui": "^4.0.9",
|
"expo-system-ui": "^4.0.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.76.9",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
"react-native-immersive-mode": "^2.0.2",
|
"react-native-immersive-mode": "^2.0.2",
|
||||||
"react-native-paper": "^5.13.1",
|
"react-native-paper": "^5.13.1",
|
||||||
|
|
@ -49,6 +44,7 @@
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-native": "^0.72.8",
|
"@types/react-native": "^0.72.8",
|
||||||
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"react-native-svg-transformer": "^1.5.0",
|
"react-native-svg-transformer": "^1.5.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
|
|
@ -4920,6 +4916,13 @@
|
||||||
"@babel/plugin-syntax-flow": "^7.12.1"
|
"@babel/plugin-syntax-flow": "^7.12.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/babel-plugin-transform-remove-console": {
|
||||||
|
"version": "6.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz",
|
||||||
|
"integrity": "sha512-88blrUrMX3SPiGkT1GnvVY8E/7A+k6oj3MNvUtTIxJflFzXTw1bHkuJ/y039ouhFMp2prRn5cQGzokViYi1dsg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/babel-preset-current-node-syntax": {
|
"node_modules/babel-preset-current-node-syntax": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
|
||||||
|
|
@ -6548,34 +6551,6 @@
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-av": {
|
|
||||||
"version": "15.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-av/-/expo-av-15.0.2.tgz",
|
|
||||||
"integrity": "sha512-AHIHXdqLgK1dfHZF0JzX3YSVySGMrWn9QtPzaVjw54FAzvXfMt4sIoq4qRL/9XWCP9+ICcCs/u3EcvmxQjrfcA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*",
|
|
||||||
"react": "*",
|
|
||||||
"react-native": "*",
|
|
||||||
"react-native-web": "*"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"react-native-web": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-blur": {
|
|
||||||
"version": "14.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.0.3.tgz",
|
|
||||||
"integrity": "sha512-BL3xnqBJbYm3Hg9t/HjNjdeY7N/q8eK5tsLYxswWG1yElISWZmMvrXYekl7XaVCPfyFyz8vQeaxd7q74ZY3Wrw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*",
|
|
||||||
"react": "*",
|
|
||||||
"react-native": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-constants": {
|
"node_modules/expo-constants": {
|
||||||
"version": "17.0.8",
|
"version": "17.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.8.tgz",
|
||||||
|
|
@ -6757,27 +6732,6 @@
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-sharing": {
|
|
||||||
"version": "13.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-13.0.1.tgz",
|
|
||||||
"integrity": "sha512-qych3Nw65wlFcnzE/gRrsdtvmdV0uF4U4qVMZBJYPG90vYyWh2QM9rp1gVu0KWOBc7N8CC2dSVYn4/BXqJy6Xw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-splash-screen": {
|
|
||||||
"version": "0.29.22",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.29.22.tgz",
|
|
||||||
"integrity": "sha512-f+bPpF06bqiuW1Fbrd3nxeaSsmTVTBEKEYe3epYt4IE6y4Ulli3qEUamMLlRQiDGuIXPU6zQlscpy2mdBUI5cA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@expo/prebuild-config": "^8.0.27"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-status-bar": {
|
"node_modules/expo-status-bar": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-2.0.1.tgz",
|
||||||
|
|
@ -10486,24 +10440,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-awesome-slider": {
|
|
||||||
"version": "2.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-native-awesome-slider/-/react-native-awesome-slider-2.9.0.tgz",
|
|
||||||
"integrity": "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"workspaces": [
|
|
||||||
"example"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 18.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "*",
|
|
||||||
"react-native": "*",
|
|
||||||
"react-native-gesture-handler": ">=2.0.0",
|
|
||||||
"react-native-reanimated": ">=3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-native-gesture-handler": {
|
"node_modules/react-native-gesture-handler": {
|
||||||
"version": "2.20.2",
|
"version": "2.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "stremio-expo",
|
"name": "nuvio",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -22,21 +22,16 @@
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"expo": "~52.0.43",
|
"expo": "~52.0.43",
|
||||||
"expo-av": "^15.0.2",
|
|
||||||
"expo-blur": "^14.0.3",
|
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
"expo-linear-gradient": "~14.0.2",
|
"expo-linear-gradient": "~14.0.2",
|
||||||
"expo-notifications": "~0.29.14",
|
"expo-notifications": "~0.29.14",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
"expo-sharing": "^13.0.1",
|
|
||||||
"expo-splash-screen": "^0.29.22",
|
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
"expo-system-ui": "^4.0.9",
|
"expo-system-ui": "^4.0.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.76.9",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
"react-native-immersive-mode": "^2.0.2",
|
"react-native-immersive-mode": "^2.0.2",
|
||||||
"react-native-paper": "^5.13.1",
|
"react-native-paper": "^5.13.1",
|
||||||
|
|
@ -50,6 +45,7 @@
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-native": "^0.72.8",
|
"@types/react-native": "^0.72.8",
|
||||||
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"react-native-svg-transformer": "^1.5.0",
|
"react-native-svg-transformer": "^1.5.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 11 KiB |
|
|
@ -1,26 +1,48 @@
|
||||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||||
|
import { StreamingContent } from '../services/catalogService';
|
||||||
|
|
||||||
interface CatalogContextType {
|
interface CatalogContextType {
|
||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
refreshCatalogs: () => void;
|
refreshCatalogs: () => void;
|
||||||
|
addToLibrary: (content: StreamingContent) => void;
|
||||||
|
removeFromLibrary: (type: string, id: string) => void;
|
||||||
|
libraryItems: StreamingContent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CatalogContext = createContext<CatalogContextType>({
|
const CatalogContext = createContext<CatalogContextType>({
|
||||||
lastUpdate: Date.now(),
|
lastUpdate: Date.now(),
|
||||||
refreshCatalogs: () => {},
|
refreshCatalogs: () => {},
|
||||||
|
addToLibrary: () => {},
|
||||||
|
removeFromLibrary: () => {},
|
||||||
|
libraryItems: []
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useCatalogContext = () => useContext(CatalogContext);
|
export const useCatalogContext = () => useContext(CatalogContext);
|
||||||
|
|
||||||
export const CatalogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const CatalogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [lastUpdate, setLastUpdate] = useState(Date.now());
|
const [lastUpdate, setLastUpdate] = useState(Date.now());
|
||||||
|
const [libraryItems, setLibraryItems] = useState<StreamingContent[]>([]);
|
||||||
|
|
||||||
const refreshCatalogs = useCallback(() => {
|
const refreshCatalogs = useCallback(() => {
|
||||||
setLastUpdate(Date.now());
|
setLastUpdate(Date.now());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const addToLibrary = useCallback((content: StreamingContent) => {
|
||||||
|
setLibraryItems(prev => [...prev, content]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeFromLibrary = useCallback((type: string, id: string) => {
|
||||||
|
setLibraryItems(prev => prev.filter(item => !(item.id === id && item.type === type)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CatalogContext.Provider value={{ lastUpdate, refreshCatalogs }}>
|
<CatalogContext.Provider value={{
|
||||||
|
lastUpdate,
|
||||||
|
refreshCatalogs,
|
||||||
|
addToLibrary,
|
||||||
|
removeFromLibrary,
|
||||||
|
libraryItems
|
||||||
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</CatalogContext.Provider>
|
</CatalogContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { tmdbService } from '../services/tmdbService';
|
||||||
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';
|
||||||
|
|
||||||
// Constants for timeouts and retries
|
// Constants for timeouts and retries
|
||||||
const API_TIMEOUT = 10000; // 10 seconds
|
const API_TIMEOUT = 10000; // 10 seconds
|
||||||
|
|
@ -31,7 +32,7 @@ const loadWithFallback = async <T>(
|
||||||
try {
|
try {
|
||||||
return await withTimeout(loadFn(), timeout, fallback);
|
return await withTimeout(loadFn(), timeout, fallback);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Loading failed, using fallback:', error);
|
logger.error('Loading failed, using fallback:', error);
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -115,9 +116,9 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams';
|
const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`🔍 [${logPrefix}:${sourceType}] Starting fetch`);
|
logger.log(`🔍 [${logPrefix}:${sourceType}] Starting fetch`);
|
||||||
const result = await promise;
|
const result = await promise;
|
||||||
console.log(`✅ [${logPrefix}:${sourceType}] Completed in ${Date.now() - sourceStartTime}ms`);
|
logger.log(`✅ [${logPrefix}:${sourceType}] Completed in ${Date.now() - sourceStartTime}ms`);
|
||||||
|
|
||||||
// If we have results, update immediately
|
// If we have results, update immediately
|
||||||
if (Object.keys(result).length > 0) {
|
if (Object.keys(result).length > 0) {
|
||||||
|
|
@ -126,7 +127,7 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
return acc + (group.streams?.length || 0);
|
return acc + (group.streams?.length || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
console.log(`📦 [${logPrefix}:${sourceType}] Found ${totalStreams} streams`);
|
logger.log(`📦 [${logPrefix}:${sourceType}] Found ${totalStreams} streams`);
|
||||||
|
|
||||||
// Update state for this source
|
// Update state for this source
|
||||||
if (isEpisode) {
|
if (isEpisode) {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
// Extend Manifest type to include logo
|
// Extend Manifest type to include logo
|
||||||
interface ExtendedManifest extends Manifest {
|
interface ExtendedManifest extends Manifest {
|
||||||
|
|
@ -58,7 +59,7 @@ const AddonsScreen = () => {
|
||||||
const installedAddons = await stremioService.getInstalledAddonsAsync();
|
const installedAddons = await stremioService.getInstalledAddonsAsync();
|
||||||
setAddons(installedAddons);
|
setAddons(installedAddons);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load addons:', error);
|
logger.error('Failed to load addons:', error);
|
||||||
Alert.alert('Error', 'Failed to load addons');
|
Alert.alert('Error', 'Failed to load addons');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -79,7 +80,7 @@ const AddonsScreen = () => {
|
||||||
setShowAddModal(false);
|
setShowAddModal(false);
|
||||||
setShowConfirmModal(true);
|
setShowConfirmModal(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch addon details:', error);
|
logger.error('Failed to fetch addon details:', error);
|
||||||
Alert.alert('Error', 'Failed to fetch addon details');
|
Alert.alert('Error', 'Failed to fetch addon details');
|
||||||
} finally {
|
} finally {
|
||||||
setInstalling(false);
|
setInstalling(false);
|
||||||
|
|
@ -98,7 +99,7 @@ const AddonsScreen = () => {
|
||||||
loadAddons();
|
loadAddons();
|
||||||
Alert.alert('Success', 'Addon installed successfully');
|
Alert.alert('Success', 'Addon installed successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to install addon:', error);
|
logger.error('Failed to install addon:', error);
|
||||||
Alert.alert('Error', 'Failed to install addon');
|
Alert.alert('Error', 'Failed to install addon');
|
||||||
} finally {
|
} finally {
|
||||||
setInstalling(false);
|
setInstalling(false);
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { format, parseISO, isThisWeek, isAfter, startOfToday, addWeeks, isBefore
|
||||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||||
import { CalendarSection } from '../components/calendar/CalendarSection';
|
import { CalendarSection } from '../components/calendar/CalendarSection';
|
||||||
import { tmdbService } from '../services/tmdbService';
|
import { tmdbService } from '../services/tmdbService';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -52,7 +53,7 @@ interface CalendarSection {
|
||||||
const CalendarScreen = () => {
|
const CalendarScreen = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { libraryItems, loading: libraryLoading } = useLibrary();
|
const { libraryItems, loading: libraryLoading } = useLibrary();
|
||||||
console.log(`[Calendar] Initial load - Library has ${libraryItems?.length || 0} items, loading: ${libraryLoading}`);
|
logger.log(`[Calendar] Initial load - Library has ${libraryItems?.length || 0} items, loading: ${libraryLoading}`);
|
||||||
const [calendarData, setCalendarData] = useState<CalendarSection[]>([]);
|
const [calendarData, setCalendarData] = useState<CalendarSection[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
@ -60,13 +61,13 @@ const CalendarScreen = () => {
|
||||||
const [filteredEpisodes, setFilteredEpisodes] = useState<CalendarEpisode[]>([]);
|
const [filteredEpisodes, setFilteredEpisodes] = useState<CalendarEpisode[]>([]);
|
||||||
|
|
||||||
const fetchCalendarData = useCallback(async () => {
|
const fetchCalendarData = useCallback(async () => {
|
||||||
console.log("[Calendar] Starting to fetch calendar data");
|
logger.log("[Calendar] Starting to fetch calendar data");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Filter for only series in library
|
// Filter for only series in library
|
||||||
const seriesItems = libraryItems.filter(item => item.type === 'series');
|
const seriesItems = libraryItems.filter(item => item.type === 'series');
|
||||||
console.log(`[Calendar] Library items: ${libraryItems.length}, Series items: ${seriesItems.length}`);
|
logger.log(`[Calendar] Library items: ${libraryItems.length}, Series items: ${seriesItems.length}`);
|
||||||
|
|
||||||
let allEpisodes: CalendarEpisode[] = [];
|
let allEpisodes: CalendarEpisode[] = [];
|
||||||
let seriesWithoutEpisodes: CalendarEpisode[] = [];
|
let seriesWithoutEpisodes: CalendarEpisode[] = [];
|
||||||
|
|
@ -74,12 +75,12 @@ const CalendarScreen = () => {
|
||||||
// For each series, fetch upcoming episodes
|
// For each series, fetch upcoming episodes
|
||||||
for (const series of seriesItems) {
|
for (const series of seriesItems) {
|
||||||
try {
|
try {
|
||||||
console.log(`[Calendar] Fetching episodes for series: ${series.name} (${series.id})`);
|
logger.log(`[Calendar] Fetching episodes for series: ${series.name} (${series.id})`);
|
||||||
const metadata = await stremioService.getMetaDetails(series.type, series.id);
|
const metadata = await stremioService.getMetaDetails(series.type, series.id);
|
||||||
console.log(`[Calendar] Metadata fetched:`, metadata ? 'success' : 'null');
|
logger.log(`[Calendar] Metadata fetched:`, metadata ? 'success' : 'null');
|
||||||
|
|
||||||
if (metadata?.videos && metadata.videos.length > 0) {
|
if (metadata?.videos && metadata.videos.length > 0) {
|
||||||
console.log(`[Calendar] Series ${series.name} has ${metadata.videos.length} videos`);
|
logger.log(`[Calendar] Series ${series.name} has ${metadata.videos.length} videos`);
|
||||||
// Filter for upcoming episodes or recently released
|
// Filter for upcoming episodes or recently released
|
||||||
const today = startOfToday();
|
const today = startOfToday();
|
||||||
const fourWeeksLater = addWeeks(today, 4);
|
const fourWeeksLater = addWeeks(today, 4);
|
||||||
|
|
@ -161,7 +162,7 @@ const CalendarScreen = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching episodes for ${series.name}:`, error);
|
logger.error(`Error fetching episodes for ${series.name}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,7 +186,7 @@ const CalendarScreen = () => {
|
||||||
!isThisWeek(parseISO(episode.releaseDate))
|
!isThisWeek(parseISO(episode.releaseDate))
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[Calendar] Episodes summary: All episodes: ${allEpisodes.length}, This Week: ${thisWeekEpisodes.length}, Upcoming: ${upcomingEpisodes.length}, Recent: ${recentEpisodes.length}, No Schedule: ${seriesWithoutEpisodes.length}`);
|
logger.log(`[Calendar] Episodes summary: All episodes: ${allEpisodes.length}, This Week: ${thisWeekEpisodes.length}, Upcoming: ${upcomingEpisodes.length}, Recent: ${recentEpisodes.length}, No Schedule: ${seriesWithoutEpisodes.length}`);
|
||||||
|
|
||||||
const sections: CalendarSection[] = [];
|
const sections: CalendarSection[] = [];
|
||||||
|
|
||||||
|
|
@ -207,7 +208,7 @@ const CalendarScreen = () => {
|
||||||
|
|
||||||
setCalendarData(sections);
|
setCalendarData(sections);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching calendar data:', error);
|
logger.error('Error fetching calendar data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
|
|
@ -216,10 +217,10 @@ const CalendarScreen = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (libraryItems.length > 0 && !libraryLoading) {
|
if (libraryItems.length > 0 && !libraryLoading) {
|
||||||
console.log(`[Calendar] Library loaded with ${libraryItems.length} items, fetching calendar data`);
|
logger.log(`[Calendar] Library loaded with ${libraryItems.length} items, fetching calendar data`);
|
||||||
fetchCalendarData();
|
fetchCalendarData();
|
||||||
} else if (!libraryLoading) {
|
} else if (!libraryLoading) {
|
||||||
console.log(`[Calendar] Library loaded but empty (${libraryItems.length} items)`);
|
logger.log(`[Calendar] Library loaded but empty (${libraryItems.length} items)`);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [libraryItems, libraryLoading, fetchCalendarData]);
|
}, [libraryItems, libraryLoading, fetchCalendarData]);
|
||||||
|
|
@ -358,11 +359,11 @@ const CalendarScreen = () => {
|
||||||
[...acc, ...section.data], [] as CalendarEpisode[]);
|
[...acc, ...section.data], [] as CalendarEpisode[]);
|
||||||
|
|
||||||
// Log when rendering with relevant state info
|
// Log when rendering with relevant state info
|
||||||
console.log(`[Calendar] Rendering: loading=${loading}, calendarData sections=${calendarData.length}, allEpisodes=${allEpisodes.length}`);
|
logger.log(`[Calendar] Rendering: loading=${loading}, calendarData sections=${calendarData.length}, allEpisodes=${allEpisodes.length}`);
|
||||||
|
|
||||||
// Handle date selection from calendar
|
// Handle date selection from calendar
|
||||||
const handleDateSelect = useCallback((date: Date) => {
|
const handleDateSelect = useCallback((date: Date) => {
|
||||||
console.log(`[Calendar] Date selected: ${format(date, 'yyyy-MM-dd')}`);
|
logger.log(`[Calendar] Date selected: ${format(date, 'yyyy-MM-dd')}`);
|
||||||
setSelectedDate(date);
|
setSelectedDate(date);
|
||||||
|
|
||||||
// Filter episodes for the selected date
|
// Filter episodes for the selected date
|
||||||
|
|
@ -372,13 +373,13 @@ const CalendarScreen = () => {
|
||||||
return isSameDay(episodeDate, date);
|
return isSameDay(episodeDate, date);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[Calendar] Filtered episodes for selected date: ${filtered.length}`);
|
logger.log(`[Calendar] Filtered episodes for selected date: ${filtered.length}`);
|
||||||
setFilteredEpisodes(filtered);
|
setFilteredEpisodes(filtered);
|
||||||
}, [allEpisodes]);
|
}, [allEpisodes]);
|
||||||
|
|
||||||
// Reset date filter
|
// Reset date filter
|
||||||
const clearDateFilter = useCallback(() => {
|
const clearDateFilter = useCallback(() => {
|
||||||
console.log(`[Calendar] Clearing date filter`);
|
logger.log(`[Calendar] Clearing date filter`);
|
||||||
setSelectedDate(null);
|
setSelectedDate(null);
|
||||||
setFilteredEpisodes([]);
|
setFilteredEpisodes([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -444,7 +445,7 @@ const CalendarScreen = () => {
|
||||||
<MaterialIcons name="arrow-back" size={24} color={colors.text} />
|
<MaterialIcons name="arrow-back" size={24} color={colors.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>Calendar</Text>
|
<Text style={styles.headerTitle}>Calendar</Text>
|
||||||
<View style={{ width: 40 }} /> {/* Empty view for balance */}
|
<View style={{ width: 40 }} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{selectedDate && filteredEpisodes.length > 0 && (
|
{selectedDate && filteredEpisodes.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { Meta, stremioService } from '../services/stremioService';
|
import { Meta, stremioService } from '../services/stremioService';
|
||||||
import { colors } from '../styles';
|
import { colors } from '../styles';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
type CatalogScreenProps = {
|
type CatalogScreenProps = {
|
||||||
route: RouteProp<RootStackParamList, 'Catalog'>;
|
route: RouteProp<RootStackParamList, 'Catalog'>;
|
||||||
|
|
@ -104,12 +105,12 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
foundItems = true;
|
foundItems = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Failed to load items from ${manifest.name} catalog ${catalog.id}:`, error);
|
logger.log(`Failed to load items from ${manifest.name} catalog ${catalog.id}:`, error);
|
||||||
// Continue with other catalogs
|
// Continue with other catalogs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Failed to process addon ${manifest.name}:`, error);
|
logger.log(`Failed to process addon ${manifest.name}:`, error);
|
||||||
// Continue with other addons
|
// Continue with other addons
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +141,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load catalog items');
|
setError(err instanceof Error ? err.message : 'Failed to load catalog items');
|
||||||
console.error('Failed to load catalog:', err);
|
logger.error('Failed to load catalog:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { colors } from '../styles';
|
||||||
import { stremioService } from '../services/stremioService';
|
import { stremioService } from '../services/stremioService';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
interface CatalogSetting {
|
interface CatalogSetting {
|
||||||
addonId: string;
|
addonId: string;
|
||||||
|
|
@ -112,7 +113,7 @@ const CatalogSettingsScreen = () => {
|
||||||
|
|
||||||
setSettings(sortedCatalogs);
|
setSettings(sortedCatalogs);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load catalog settings:', error);
|
logger.error('Failed to load catalog settings:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +132,7 @@ const CatalogSettingsScreen = () => {
|
||||||
await AsyncStorage.setItem(CATALOG_SETTINGS_KEY, JSON.stringify(settingsObj));
|
await AsyncStorage.setItem(CATALOG_SETTINGS_KEY, JSON.stringify(settingsObj));
|
||||||
refreshCatalogs(); // Trigger catalog refresh after saving settings
|
refreshCatalogs(); // Trigger catalog refresh after saving settings
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save catalog settings:', error);
|
logger.error('Failed to save catalog settings:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { Image } from 'expo-image';
|
||||||
import Animated, { FadeIn, FadeOut, SlideInRight, Layout } from 'react-native-reanimated';
|
import Animated, { FadeIn, FadeOut, SlideInRight, Layout } from 'react-native-reanimated';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -307,7 +308,7 @@ const DiscoverScreen = () => {
|
||||||
setCatalogs([{ genre, items: content }]);
|
setCatalogs([{ genre, items: content }]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load content:', error);
|
logger.error('Failed to load content:', error);
|
||||||
setCatalogs([]);
|
setCatalogs([]);
|
||||||
setAllContent([]);
|
setAllContent([]);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ import {
|
||||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||||
import { ThisWeekSection } from '../components/home/ThisWeekSection';
|
import { ThisWeekSection } from '../components/home/ThisWeekSection';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
|
import { tmdbService } from '../services/tmdbService';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
// Define interfaces for our data
|
// Define interfaces for our data
|
||||||
interface Category {
|
interface Category {
|
||||||
|
|
@ -213,6 +215,7 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
||||||
const [localItem, setLocalItem] = useState(initialItem);
|
const [localItem, setLocalItem] = useState(initialItem);
|
||||||
const [isWatched, setIsWatched] = useState(false);
|
const [isWatched, setIsWatched] = useState(false);
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
const handleLongPress = useCallback(() => {
|
const handleLongPress = useCallback(() => {
|
||||||
setMenuVisible(true);
|
setMenuVisible(true);
|
||||||
|
|
@ -276,11 +279,24 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
transition={200}
|
transition={200}
|
||||||
cachePolicy="memory-disk"
|
cachePolicy="memory-disk"
|
||||||
|
recyclingKey={`poster-${localItem.id}`}
|
||||||
|
onLoadStart={() => {
|
||||||
|
setImageLoaded(false);
|
||||||
|
setImageError(false);
|
||||||
|
}}
|
||||||
onLoadEnd={() => setImageLoaded(true)}
|
onLoadEnd={() => setImageLoaded(true)}
|
||||||
|
onError={() => {
|
||||||
|
setImageError(true);
|
||||||
|
setImageLoaded(true);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{!imageLoaded && (
|
{(!imageLoaded || imageError) && (
|
||||||
<View style={[styles.loadingOverlay, { backgroundColor: colors.elevation2 }]}>
|
<View style={[styles.loadingOverlay, { backgroundColor: colors.elevation2 }]}>
|
||||||
<ActivityIndicator color={colors.primary} size="small" />
|
{!imageError ? (
|
||||||
|
<ActivityIndicator color={colors.primary} size="small" />
|
||||||
|
) : (
|
||||||
|
<MaterialIcons name="broken-image" size={24} color={colors.lightGray} />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{isWatched && (
|
{isWatched && (
|
||||||
|
|
@ -352,6 +368,29 @@ const SkeletonFeatured = () => (
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add genre mapping
|
||||||
|
const GENRE_MAP: { [key: number]: string } = {
|
||||||
|
28: 'Action',
|
||||||
|
12: 'Adventure',
|
||||||
|
16: 'Animation',
|
||||||
|
35: 'Comedy',
|
||||||
|
80: 'Crime',
|
||||||
|
99: 'Documentary',
|
||||||
|
18: 'Drama',
|
||||||
|
10751: 'Family',
|
||||||
|
14: 'Fantasy',
|
||||||
|
36: 'History',
|
||||||
|
27: 'Horror',
|
||||||
|
10402: 'Music',
|
||||||
|
9648: 'Mystery',
|
||||||
|
10749: 'Romance',
|
||||||
|
878: 'Sci-Fi',
|
||||||
|
10770: 'TV Movie',
|
||||||
|
53: 'Thriller',
|
||||||
|
10752: 'War',
|
||||||
|
37: 'Western'
|
||||||
|
};
|
||||||
|
|
||||||
const HomeScreen = () => {
|
const HomeScreen = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
const isDarkMode = useColorScheme() === 'dark';
|
||||||
|
|
@ -366,6 +405,39 @@ const HomeScreen = () => {
|
||||||
const maxRetries = 3;
|
const maxRetries = 3;
|
||||||
const { lastUpdate } = useCatalogContext();
|
const { lastUpdate } = useCatalogContext();
|
||||||
const [isSaved, setIsSaved] = useState(false);
|
const [isSaved, setIsSaved] = useState(false);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const currentIndexRef = useRef(0);
|
||||||
|
|
||||||
|
// Add auto-rotation effect
|
||||||
|
useEffect(() => {
|
||||||
|
if (allFeaturedContent.length === 0) return;
|
||||||
|
|
||||||
|
const rotateContent = () => {
|
||||||
|
currentIndexRef.current = (currentIndexRef.current + 1) % allFeaturedContent.length;
|
||||||
|
setFeaturedContent(allFeaturedContent[currentIndexRef.current]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const intervalId = setInterval(rotateContent, 15000); // 15 seconds
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [allFeaturedContent]);
|
||||||
|
|
||||||
|
// Cleanup function for ongoing operations
|
||||||
|
const cleanup = useCallback(() => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
}, [cleanup]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
StatusBar.setTranslucent(true);
|
StatusBar.setTranslucent(true);
|
||||||
|
|
@ -386,23 +458,9 @@ const HomeScreen = () => {
|
||||||
};
|
};
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
// Function to rotate featured content
|
|
||||||
const rotateFeaturedContent = useCallback(() => {
|
|
||||||
if (allFeaturedContent.length > 0) {
|
|
||||||
const currentIndex = allFeaturedContent.findIndex(item => item.id === featuredContent?.id);
|
|
||||||
const nextIndex = (currentIndex + 1) % allFeaturedContent.length;
|
|
||||||
setFeaturedContent(allFeaturedContent[nextIndex]);
|
|
||||||
}
|
|
||||||
}, [allFeaturedContent, featuredContent]);
|
|
||||||
|
|
||||||
// Set up rotation interval
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(rotateFeaturedContent, 20000); // Rotate every 20 seconds
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [rotateFeaturedContent]);
|
|
||||||
|
|
||||||
// Function to preload images
|
|
||||||
const preloadImages = useCallback(async (content: StreamingContent[]) => {
|
const preloadImages = useCallback(async (content: StreamingContent[]) => {
|
||||||
|
if (!content.length) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoadingImages(true);
|
setLoadingImages(true);
|
||||||
const imagePromises = content.map(item => {
|
const imagePromises = content.map(item => {
|
||||||
|
|
@ -428,108 +486,112 @@ const HomeScreen = () => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadContent = useCallback(async (forceRefresh = false) => {
|
const loadFeaturedContent = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (!forceRefresh && !loading && !refreshing) {
|
const trendingResults = await tmdbService.getTrending('movie', 'day');
|
||||||
setLoading(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to delay execution
|
if (trendingResults.length > 0) {
|
||||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
const formattedContent: StreamingContent[] = trendingResults
|
||||||
|
.filter(item => item.title || item.name) // Filter out items without a name
|
||||||
// Try loading content with retries
|
.map(item => {
|
||||||
let attempt = 0;
|
const yearString = (item.release_date || item.first_air_date)?.substring(0, 4);
|
||||||
while (attempt < maxRetries) {
|
return {
|
||||||
try {
|
id: `tmdb:${item.id}`,
|
||||||
// Clear existing catalogs when forcing refresh
|
type: 'movie',
|
||||||
if (forceRefresh) {
|
name: item.title || item.name || 'Unknown Title',
|
||||||
setCatalogs([]);
|
poster: tmdbService.getImageUrl(item.poster_path) || '',
|
||||||
setAllFeaturedContent([]);
|
banner: tmdbService.getImageUrl(item.backdrop_path) || '',
|
||||||
setFeaturedContent(null);
|
logo: item.external_ids?.imdb_id ? `https://images.metahub.space/logo/medium/${item.external_ids.imdb_id}/img` : undefined,
|
||||||
}
|
description: item.overview || '',
|
||||||
|
year: yearString ? parseInt(yearString, 10) : undefined,
|
||||||
// Load catalogs from service
|
genres: item.genre_ids.map(id => GENRE_MAP[id] || id.toString()),
|
||||||
const homeCatalogs = await catalogService.getHomeCatalogs();
|
inLibrary: false,
|
||||||
|
};
|
||||||
// If no catalogs found, wait and retry
|
|
||||||
if (homeCatalogs.length === 0) {
|
|
||||||
attempt++;
|
|
||||||
console.log(`No catalogs found, retrying... (attempt ${attempt})`);
|
|
||||||
await delay(2000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a map to store unique catalogs by their content
|
|
||||||
const uniqueCatalogsMap = new Map();
|
|
||||||
|
|
||||||
homeCatalogs.forEach(catalog => {
|
|
||||||
const contentKey = catalog.items.map(item => item.id).sort().join(',');
|
|
||||||
if (!uniqueCatalogsMap.has(contentKey)) {
|
|
||||||
uniqueCatalogsMap.set(contentKey, catalog);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const uniqueCatalogs = Array.from(uniqueCatalogsMap.values());
|
setAllFeaturedContent(formattedContent);
|
||||||
const popularCatalog = uniqueCatalogs.find(catalog =>
|
// Randomly select a featured item
|
||||||
catalog.name.toLowerCase().includes('popular') ||
|
const randomIndex = Math.floor(Math.random() * formattedContent.length);
|
||||||
catalog.name.toLowerCase().includes('top') ||
|
setFeaturedContent(formattedContent[randomIndex]);
|
||||||
catalog.id.toLowerCase().includes('top')
|
}
|
||||||
);
|
} catch (error) {
|
||||||
|
logger.error('Failed to load featured content:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Set catalogs showing all unique content
|
const loadCatalogs = useCallback(async () => {
|
||||||
setCatalogs(uniqueCatalogs);
|
// Create new abort controller for this load operation
|
||||||
|
cleanup();
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
const signal = abortControllerRef.current.signal;
|
||||||
|
|
||||||
// Set featured content and preload images
|
try {
|
||||||
if (popularCatalog && popularCatalog.items.length > 0) {
|
// Load catalogs from service
|
||||||
setAllFeaturedContent(popularCatalog.items);
|
const homeCatalogs = await catalogService.getHomeCatalogs();
|
||||||
const randomIndex = Math.floor(Math.random() * popularCatalog.items.length);
|
|
||||||
setFeaturedContent(popularCatalog.items[randomIndex]);
|
if (signal.aborted) return;
|
||||||
|
|
||||||
// Preload images for featured content and first few items of each catalog
|
|
||||||
const contentToPreload = [
|
|
||||||
...popularCatalog.items.slice(0, 5),
|
|
||||||
...uniqueCatalogs.flatMap(catalog => catalog.items.slice(0, 3))
|
|
||||||
];
|
|
||||||
await preloadImages(contentToPreload);
|
|
||||||
} else if (uniqueCatalogs.length > 0 && uniqueCatalogs[0].items.length > 0) {
|
|
||||||
setAllFeaturedContent(uniqueCatalogs[0].items);
|
|
||||||
setFeaturedContent(uniqueCatalogs[0].items[0]);
|
|
||||||
|
|
||||||
// Preload images for first catalog
|
|
||||||
await preloadImages(uniqueCatalogs[0].items.slice(0, 5));
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
// If no catalogs found, wait and retry
|
||||||
} catch (error) {
|
if (!homeCatalogs?.length) {
|
||||||
attempt++;
|
console.log('No catalogs found');
|
||||||
console.error(`Failed to load content (attempt ${attempt}):`, error);
|
return;
|
||||||
if (attempt < maxRetries) {
|
|
||||||
await delay(2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('All attempts to load content failed');
|
// Create a map to store unique catalogs by their content
|
||||||
setCatalogs([]);
|
const uniqueCatalogsMap = new Map();
|
||||||
setAllFeaturedContent([]);
|
|
||||||
setFeaturedContent(null);
|
homeCatalogs.forEach(catalog => {
|
||||||
|
const contentKey = catalog.items.map(item => item.id).sort().join(',');
|
||||||
|
if (!uniqueCatalogsMap.has(contentKey)) {
|
||||||
|
uniqueCatalogsMap.set(contentKey, catalog);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (signal.aborted) return;
|
||||||
|
|
||||||
|
const uniqueCatalogs = Array.from(uniqueCatalogsMap.values());
|
||||||
|
setCatalogs(uniqueCatalogs);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in loadCatalogs:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (!signal.aborted) {
|
||||||
setRefreshing(false);
|
setLoading(false);
|
||||||
|
setRefreshing(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [maxRetries, preloadImages, loading, refreshing]);
|
}, [maxRetries, cleanup]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadInitialData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
loadFeaturedContent(),
|
||||||
|
loadCatalogs(),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error loading initial data:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadInitialData();
|
||||||
|
}, [loadFeaturedContent, loadCatalogs, lastUpdate]);
|
||||||
|
|
||||||
// Reset retry count when refreshing manually
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
loadContent(true);
|
Promise.all([
|
||||||
}, [loadContent]);
|
loadFeaturedContent(),
|
||||||
|
loadCatalogs(),
|
||||||
// Load content initially and when lastUpdate changes
|
]).catch(error => {
|
||||||
useEffect(() => {
|
logger.error('Error during refresh:', error);
|
||||||
loadContent(true);
|
}).finally(() => {
|
||||||
}, [loadContent, lastUpdate]);
|
setRefreshing(false);
|
||||||
|
});
|
||||||
|
}, [loadFeaturedContent, loadCatalogs]);
|
||||||
|
|
||||||
// Check if content is in library
|
// Check if content is in library
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -668,10 +730,19 @@ const HomeScreen = () => {
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.infoButton}
|
style={styles.infoButton}
|
||||||
onPress={() => navigation.navigate('Metadata', {
|
onPress={async () => {
|
||||||
id: featuredContent?.id,
|
if (featuredContent) {
|
||||||
type: featuredContent?.type
|
// Convert TMDB ID to Stremio ID
|
||||||
})}
|
const tmdbId = featuredContent.id.replace('tmdb:', '');
|
||||||
|
const stremioId = await catalogService.getStremioId(featuredContent.type, tmdbId);
|
||||||
|
if (stremioId) {
|
||||||
|
navigation.navigate('Metadata', {
|
||||||
|
id: stremioId,
|
||||||
|
type: featuredContent.type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="info-outline" size={24} color={colors.white} />
|
<MaterialIcons name="info-outline" size={24} color={colors.white} />
|
||||||
<Text style={styles.infoButtonText}>Info</Text>
|
<Text style={styles.infoButtonText}>Info</Text>
|
||||||
|
|
@ -719,7 +790,7 @@ const HomeScreen = () => {
|
||||||
<FlatList
|
<FlatList
|
||||||
data={item.items}
|
data={item.items}
|
||||||
renderItem={renderContentItem}
|
renderItem={renderContentItem}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => `${item.id}-${item.type}`}
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.catalogList}
|
contentContainerStyle={styles.catalogList}
|
||||||
|
|
@ -727,6 +798,15 @@ const HomeScreen = () => {
|
||||||
decelerationRate="fast"
|
decelerationRate="fast"
|
||||||
snapToAlignment="start"
|
snapToAlignment="start"
|
||||||
ItemSeparatorComponent={() => <View style={{ width: 10 }} />}
|
ItemSeparatorComponent={() => <View style={{ width: 10 }} />}
|
||||||
|
initialNumToRender={4}
|
||||||
|
maxToRenderPerBatch={4}
|
||||||
|
windowSize={5}
|
||||||
|
removeClippedSubviews={Platform.OS === 'android'}
|
||||||
|
getItemLayout={(data, index) => ({
|
||||||
|
length: POSTER_WIDTH + 10,
|
||||||
|
offset: (POSTER_WIDTH + 10) * index,
|
||||||
|
index,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
@ -780,6 +860,10 @@ const HomeScreen = () => {
|
||||||
renderItem={renderCatalog}
|
renderItem={renderCatalog}
|
||||||
keyExtractor={(item, index) => `${item.addon}-${item.id}-${index}`}
|
keyExtractor={(item, index) => `${item.addon}-${item.id}-${index}`}
|
||||||
scrollEnabled={false}
|
scrollEnabled={false}
|
||||||
|
removeClippedSubviews={false}
|
||||||
|
initialNumToRender={3}
|
||||||
|
maxToRenderPerBatch={3}
|
||||||
|
windowSize={5}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.emptyCatalog}>
|
<View style={styles.emptyCatalog}>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||||
import { catalogService } from '../services/catalogService';
|
import { catalogService } from '../services/catalogService';
|
||||||
import type { StreamingContent } from '../services/catalogService';
|
import type { StreamingContent } from '../services/catalogService';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface LibraryItem extends StreamingContent {
|
interface LibraryItem extends StreamingContent {
|
||||||
|
|
@ -103,7 +104,7 @@ const LibraryScreen = () => {
|
||||||
const items = await catalogService.getLibraryItems();
|
const items = await catalogService.getLibraryItems();
|
||||||
setLibraryItems(items);
|
setLibraryItems(items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load library:', error);
|
logger.error('Failed to load library:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import { NavigationProp } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { TMDBService } from '../services/tmdbService';
|
import { TMDBService } from '../services/tmdbService';
|
||||||
import { storageService } from '../services/storageService';
|
import { storageService } from '../services/storageService';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -54,7 +55,7 @@ const springConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add debug log for storageService
|
// Add debug log for storageService
|
||||||
console.log('[MetadataScreen] StorageService instance:', storageService);
|
logger.log('[MetadataScreen] StorageService instance:', storageService);
|
||||||
|
|
||||||
const MetadataScreen = () => {
|
const MetadataScreen = () => {
|
||||||
const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string }>, string>>();
|
const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string }>, string>>();
|
||||||
|
|
@ -101,8 +102,15 @@ const MetadataScreen = () => {
|
||||||
episodeId?: string;
|
episodeId?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
// Add new animated value for creator height
|
||||||
|
const creatorHeight = useSharedValue(0);
|
||||||
|
|
||||||
|
// Add new animated value for watch progress
|
||||||
|
const watchProgressHeight = useSharedValue(0);
|
||||||
|
const watchProgressOpacity = useSharedValue(0);
|
||||||
|
|
||||||
// Debug log for route params
|
// Debug log for route params
|
||||||
console.log('[MetadataScreen] Component mounted with route params:', { id, type, episodeId });
|
logger.log('[MetadataScreen] Component mounted with route params:', { id, type, episodeId });
|
||||||
|
|
||||||
// Function to get episode details from episodeId
|
// Function to get episode details from episodeId
|
||||||
const getEpisodeDetails = useCallback((episodeId: string) => {
|
const getEpisodeDetails = useCallback((episodeId: string) => {
|
||||||
|
|
@ -260,7 +268,7 @@ const MetadataScreen = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[MetadataScreen] Error loading watch progress:', error);
|
logger.error('[MetadataScreen] Error loading watch progress:', error);
|
||||||
setWatchProgress(null);
|
setWatchProgress(null);
|
||||||
}
|
}
|
||||||
}, [id, type, episodeId, episodes]);
|
}, [id, type, episodeId, episodes]);
|
||||||
|
|
@ -292,9 +300,70 @@ const MetadataScreen = () => {
|
||||||
return 'Resume';
|
return 'Resume';
|
||||||
}, [watchProgress]);
|
}, [watchProgress]);
|
||||||
|
|
||||||
// Update the watch progress display
|
// Add effect to animate watch progress when it changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (watchProgress && watchProgress.duration > 0) {
|
||||||
|
watchProgressHeight.value = withSpring(48, {
|
||||||
|
mass: 0.3,
|
||||||
|
stiffness: 120,
|
||||||
|
damping: 15,
|
||||||
|
velocity: 0.5
|
||||||
|
});
|
||||||
|
watchProgressOpacity.value = withSpring(1, {
|
||||||
|
mass: 0.2,
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 12
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
watchProgressHeight.value = withSpring(0, {
|
||||||
|
mass: 0.3,
|
||||||
|
stiffness: 120,
|
||||||
|
damping: 15
|
||||||
|
});
|
||||||
|
watchProgressOpacity.value = withSpring(0, {
|
||||||
|
mass: 0.2,
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 12
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [watchProgress]);
|
||||||
|
|
||||||
|
// Add animated style for watch progress
|
||||||
|
const watchProgressAnimatedStyle = useAnimatedStyle(() => {
|
||||||
|
const progress = interpolate(
|
||||||
|
watchProgressHeight.value,
|
||||||
|
[0, 48],
|
||||||
|
[0, 1],
|
||||||
|
Extrapolate.CLAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: watchProgressHeight.value,
|
||||||
|
opacity: watchProgressOpacity.value,
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateY: interpolate(
|
||||||
|
progress,
|
||||||
|
[0, 1],
|
||||||
|
[-8, 0],
|
||||||
|
Extrapolate.CLAMP
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: interpolate(
|
||||||
|
progress,
|
||||||
|
[0, 1],
|
||||||
|
[0.95, 1],
|
||||||
|
Extrapolate.CLAMP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the watch progress render function
|
||||||
const renderWatchProgress = () => {
|
const renderWatchProgress = () => {
|
||||||
if (!watchProgress) {
|
if (!watchProgress || watchProgress.duration === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,7 +379,7 @@ const MetadataScreen = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.watchProgressContainer}>
|
<Animated.View style={[styles.watchProgressContainer, watchProgressAnimatedStyle]}>
|
||||||
<View style={styles.watchProgressBar}>
|
<View style={styles.watchProgressBar}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -322,7 +391,7 @@ const MetadataScreen = () => {
|
||||||
<Text style={styles.watchProgressText}>
|
<Text style={styles.watchProgressText}>
|
||||||
{progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`}{episodeInfo} • Last watched on {formattedTime}
|
{progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`}{episodeInfo} • Last watched on {formattedTime}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -366,7 +435,7 @@ const MetadataScreen = () => {
|
||||||
if (tmdbId) {
|
if (tmdbId) {
|
||||||
navigation.navigate('ShowRatings', { showId: tmdbId });
|
navigation.navigate('ShowRatings', { showId: tmdbId });
|
||||||
} else {
|
} else {
|
||||||
console.error('Could not find TMDB ID for show');
|
logger.error('Could not find TMDB ID for show');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -408,8 +477,7 @@ const MetadataScreen = () => {
|
||||||
}, [navigation, id, type, episodes, episodeId, watchProgress]);
|
}, [navigation, id, type, episodes, episodeId, watchProgress]);
|
||||||
|
|
||||||
const handleSelectCastMember = (castMember: any) => {
|
const handleSelectCastMember = (castMember: any) => {
|
||||||
// TODO: Implement cast member selection
|
logger.log('Cast member selected:', castMember);
|
||||||
console.log('Cast member selected:', castMember);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEpisodeSelect = (episode: Episode) => {
|
const handleEpisodeSelect = (episode: Episode) => {
|
||||||
|
|
@ -483,6 +551,46 @@ const MetadataScreen = () => {
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Add animated style for creator container
|
||||||
|
const creatorAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
|
maxHeight: creatorHeight.value,
|
||||||
|
opacity: interpolate(
|
||||||
|
creatorHeight.value,
|
||||||
|
[0, 24],
|
||||||
|
[0, 1],
|
||||||
|
Extrapolate.CLAMP
|
||||||
|
),
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateY: interpolate(
|
||||||
|
creatorHeight.value,
|
||||||
|
[0, 24],
|
||||||
|
[-8, 0],
|
||||||
|
Extrapolate.CLAMP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add effect to animate height when metadata changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (metadata?.directors?.length || metadata?.creators?.length) {
|
||||||
|
creatorHeight.value = withSpring(24, {
|
||||||
|
mass: 0.5,
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 12,
|
||||||
|
velocity: 0.4
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
creatorHeight.value = withSpring(0, {
|
||||||
|
mass: 0.5,
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 12,
|
||||||
|
velocity: 0.4
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [metadata?.directors, metadata?.creators]);
|
||||||
|
|
||||||
// Debug logs for director/creator data
|
// Debug logs for director/creator data
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (metadata && metadata.id) {
|
if (metadata && metadata.id) {
|
||||||
|
|
@ -493,7 +601,7 @@ const MetadataScreen = () => {
|
||||||
|
|
||||||
if (tmdbId) {
|
if (tmdbId) {
|
||||||
const credits = await tmdb.getCredits(tmdbId, type);
|
const credits = await tmdb.getCredits(tmdbId, type);
|
||||||
console.log("Credits data structure:", JSON.stringify(credits).substring(0, 300));
|
logger.log("Credits data structure:", JSON.stringify(credits).substring(0, 300));
|
||||||
|
|
||||||
// Extract directors for movies
|
// Extract directors for movies
|
||||||
if (type === 'movie' && credits.crew) {
|
if (type === 'movie' && credits.crew) {
|
||||||
|
|
@ -507,7 +615,7 @@ const MetadataScreen = () => {
|
||||||
...metadata,
|
...metadata,
|
||||||
directors
|
directors
|
||||||
});
|
});
|
||||||
console.log("Updated directors:", directors);
|
logger.log("Updated directors:", directors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -528,12 +636,12 @@ const MetadataScreen = () => {
|
||||||
...metadata,
|
...metadata,
|
||||||
creators: creators.slice(0, 3) // Limit to first 3 creators
|
creators: creators.slice(0, 3) // Limit to first 3 creators
|
||||||
});
|
});
|
||||||
console.log("Updated creators:", creators.slice(0, 3));
|
logger.log("Updated creators:", creators.slice(0, 3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching crew data:', error);
|
logger.error('Error fetching crew data:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -731,22 +839,26 @@ const MetadataScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Creator/Director Info */}
|
{/* Creator/Director Info */}
|
||||||
{((metadata.directors && metadata.directors.length > 0) || (metadata.creators && metadata.creators.length > 0)) && (
|
<Animated.View
|
||||||
<View style={styles.creatorContainer}>
|
style={[
|
||||||
{metadata.directors && metadata.directors.length > 0 && (
|
styles.creatorContainer,
|
||||||
<View style={styles.creatorSection}>
|
creatorAnimatedStyle,
|
||||||
<Text style={styles.creatorLabel}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text>
|
{ minHeight: (metadata?.directors?.length || metadata?.creators?.length) ? 'auto' : 0 }
|
||||||
<Text style={styles.creatorText}>{metadata.directors.join(', ')}</Text>
|
]}
|
||||||
</View>
|
>
|
||||||
)}
|
{metadata.directors && metadata.directors.length > 0 && (
|
||||||
{metadata.creators && metadata.creators.length > 0 && (
|
<View style={styles.creatorSection}>
|
||||||
<View style={styles.creatorSection}>
|
<Text style={styles.creatorLabel}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text>
|
||||||
<Text style={styles.creatorLabel}>Creator{metadata.creators.length > 1 ? 's' : ''}:</Text>
|
<Text style={styles.creatorText}>{metadata.directors.join(', ')}</Text>
|
||||||
<Text style={styles.creatorText}>{metadata.creators.join(', ')}</Text>
|
</View>
|
||||||
</View>
|
)}
|
||||||
)}
|
{metadata.creators && metadata.creators.length > 0 && (
|
||||||
</View>
|
<View style={styles.creatorSection}>
|
||||||
)}
|
<Text style={styles.creatorLabel}>Creator{metadata.creators.length > 1 ? 's' : ''}:</Text>
|
||||||
|
<Text style={styles.creatorText}>{metadata.creators.join(', ')}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{metadata.description && (
|
{metadata.description && (
|
||||||
|
|
@ -1108,36 +1220,41 @@ const styles = StyleSheet.create({
|
||||||
creatorContainer: {
|
creatorContainer: {
|
||||||
marginBottom: 2,
|
marginBottom: 2,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
|
overflow: 'hidden'
|
||||||
},
|
},
|
||||||
creatorSection: {
|
creatorSection: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 8,
|
marginBottom: 4,
|
||||||
|
height: 20
|
||||||
},
|
},
|
||||||
creatorLabel: {
|
creatorLabel: {
|
||||||
color: colors.white,
|
color: colors.white,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
|
lineHeight: 20
|
||||||
},
|
},
|
||||||
creatorText: {
|
creatorText: {
|
||||||
color: colors.lightGray,
|
color: colors.lightGray,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
lineHeight: 20
|
||||||
},
|
},
|
||||||
watchProgressContainer: {
|
watchProgressContainer: {
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
marginBottom: 16,
|
marginBottom: 12,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
overflow: 'hidden'
|
||||||
},
|
},
|
||||||
watchProgressBar: {
|
watchProgressBar: {
|
||||||
width: '80%',
|
width: '75%',
|
||||||
height: 3,
|
height: 3,
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginBottom: 8,
|
marginBottom: 6
|
||||||
},
|
},
|
||||||
watchProgressFill: {
|
watchProgressFill: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|
@ -1148,6 +1265,8 @@ const styles = StyleSheet.create({
|
||||||
color: colors.textMuted,
|
color: colors.textMuted,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
opacity: 0.9,
|
||||||
|
letterSpacing: 0.2
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { colors } from '../styles/colors';
|
||||||
import { notificationService, NotificationSettings } from '../services/notificationService';
|
import { notificationService, NotificationSettings } from '../services/notificationService';
|
||||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
const NotificationSettingsScreen = () => {
|
const NotificationSettingsScreen = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
@ -36,7 +37,7 @@ const NotificationSettingsScreen = () => {
|
||||||
const savedSettings = await notificationService.getSettings();
|
const savedSettings = await notificationService.getSettings();
|
||||||
setSettings(savedSettings);
|
setSettings(savedSettings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading notification settings:', error);
|
logger.error('Error loading notification settings:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +85,7 @@ const NotificationSettingsScreen = () => {
|
||||||
// Update local state
|
// Update local state
|
||||||
setSettings(updatedSettings);
|
setSettings(updatedSettings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating notification settings:', error);
|
logger.error('Error updating notification settings:', error);
|
||||||
Alert.alert('Error', 'Failed to update notification settings');
|
Alert.alert('Error', 'Failed to update notification settings');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -111,7 +112,7 @@ const NotificationSettingsScreen = () => {
|
||||||
await notificationService.cancelAllNotifications();
|
await notificationService.cancelAllNotifications();
|
||||||
Alert.alert('Success', 'All notifications have been reset');
|
Alert.alert('Success', 'All notifications have been reset');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error resetting notifications:', error);
|
logger.error('Error resetting notifications:', error);
|
||||||
Alert.alert('Error', 'Failed to reset notifications');
|
Alert.alert('Error', 'Failed to reset notifications');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -147,7 +148,7 @@ const NotificationSettingsScreen = () => {
|
||||||
Alert.alert('Error', 'Failed to schedule test notification. Make sure notifications are enabled.');
|
Alert.alert('Error', 'Failed to schedule test notification. Make sure notifications are enabled.');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error scheduling test notification:', error);
|
logger.error('Error scheduling test notification:', error);
|
||||||
Alert.alert('Error', 'Failed to schedule test notification');
|
Alert.alert('Error', 'Failed to schedule test notification');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import debounce from 'lodash/debounce';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import Animated, { FadeIn, FadeOut, SlideInRight } from 'react-native-reanimated';
|
import Animated, { FadeIn, FadeOut, SlideInRight } from 'react-native-reanimated';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const HORIZONTAL_ITEM_WIDTH = width * 0.3;
|
const HORIZONTAL_ITEM_WIDTH = width * 0.3;
|
||||||
|
|
@ -117,7 +118,7 @@ const SearchScreen = () => {
|
||||||
setRecentSearches(JSON.parse(savedSearches));
|
setRecentSearches(JSON.parse(savedSearches));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load recent searches:', error);
|
logger.error('Failed to load recent searches:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -131,7 +132,7 @@ const SearchScreen = () => {
|
||||||
setRecentSearches(newRecentSearches);
|
setRecentSearches(newRecentSearches);
|
||||||
await AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
|
await AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save recent search:', error);
|
logger.error('Failed to save recent search:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -148,7 +149,7 @@ const SearchScreen = () => {
|
||||||
setResults(searchResults);
|
setResults(searchResults);
|
||||||
await saveRecentSearch(searchQuery);
|
await saveRecentSearch(searchQuery);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search failed:', error);
|
logger.error('Search failed:', error);
|
||||||
setResults([]);
|
setResults([]);
|
||||||
} finally {
|
} finally {
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { RouteProp } from '@react-navigation/native';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Animated, { FadeIn, SlideInRight, withTiming, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
import Animated, { FadeIn, SlideInRight, withTiming, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
type RootStackParamList = {
|
type RootStackParamList = {
|
||||||
ShowRatings: { showId: number };
|
ShowRatings: { showId: number };
|
||||||
|
|
@ -228,7 +229,7 @@ const ShowRatingsScreen = ({ route }: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching TVMaze data:', error);
|
logger.error('Error fetching TVMaze data:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -273,7 +274,7 @@ const ShowRatingsScreen = ({ route }: Props) => {
|
||||||
setLoadingProgress((loadedCount / totalToLoad) * 100);
|
setLoadingProgress((loadedCount / totalToLoad) * 100);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading more seasons:', error);
|
logger.error('Error loading more seasons:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingProgress(0);
|
setLoadingProgress(0);
|
||||||
setLoadingSeasons(false);
|
setLoadingSeasons(false);
|
||||||
|
|
@ -314,7 +315,7 @@ const ShowRatingsScreen = ({ route }: Props) => {
|
||||||
setVisibleSeasonRange({ start: 0, end: initialEnd });
|
setVisibleSeasonRange({ start: 0, end: initialEnd });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching show data:', error);
|
logger.error('Error fetching show data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import Animated, {
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { torrentService } from '../services/torrentService';
|
import { torrentService } from '../services/torrentService';
|
||||||
import { TorrentProgress } from '../services/torrentService';
|
import { TorrentProgress } from '../services/torrentService';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906';
|
const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906';
|
||||||
const HDR_ICON = 'https://uxwing.com/wp-content/themes/uxwing/download/video-photography-multimedia/hdr-icon.png';
|
const HDR_ICON = 'https://uxwing.com/wp-content/themes/uxwing/download/video-photography-multimedia/hdr-icon.png';
|
||||||
|
|
@ -77,7 +78,7 @@ const StreamCard = memo(({ stream, onPress, index, torrentProgress, isLoading, s
|
||||||
, [index]);
|
, [index]);
|
||||||
|
|
||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
console.log('StreamCard pressed:', {
|
logger.log('StreamCard pressed:', {
|
||||||
isTorrent,
|
isTorrent,
|
||||||
isDebrid,
|
isDebrid,
|
||||||
hasProgress: !!torrentProgress,
|
hasProgress: !!torrentProgress,
|
||||||
|
|
@ -310,7 +311,7 @@ export const StreamsScreen = () => {
|
||||||
// Monitor streams loading start
|
// Monitor streams loading start
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingStreams || loadingEpisodeStreams) {
|
if (loadingStreams || loadingEpisodeStreams) {
|
||||||
console.log("⏱️ Stream loading started");
|
logger.log("⏱️ Stream loading started");
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
setLoadStartTime(now);
|
setLoadStartTime(now);
|
||||||
setProviderLoadTimes({});
|
setProviderLoadTimes({});
|
||||||
|
|
@ -368,7 +369,7 @@ export const StreamsScreen = () => {
|
||||||
// Update provider status when new streams appear
|
// Update provider status when new streams appear
|
||||||
setProviderStatus(prev => {
|
setProviderStatus(prev => {
|
||||||
const loadTime = now - loadStartTime;
|
const loadTime = now - loadStartTime;
|
||||||
console.log(`✅ Provider "${parentProvider}" loaded successfully after ${loadTime}ms with ${streams[provider].streams.length} streams`);
|
logger.log(`✅ Provider "${parentProvider}" loaded successfully after ${loadTime}ms with ${streams[provider].streams.length} streams`);
|
||||||
|
|
||||||
// Only update if it was previously loading
|
// Only update if it was previously loading
|
||||||
if (prev[parentProvider]?.loading) {
|
if (prev[parentProvider]?.loading) {
|
||||||
|
|
@ -409,7 +410,7 @@ export const StreamsScreen = () => {
|
||||||
timeCompleted: Date.now()
|
timeCompleted: Date.now()
|
||||||
};
|
};
|
||||||
updated = true;
|
updated = true;
|
||||||
console.log(`⚠️ Provider "${provider}" timed out or failed`);
|
logger.log(`⚠️ Provider "${provider}" timed out or failed`);
|
||||||
|
|
||||||
// Update the simpler loading state
|
// Update the simpler loading state
|
||||||
setLoadingProviders((prevLoading: {[key: string]: boolean}) => ({...prevLoading, [provider]: false}));
|
setLoadingProviders((prevLoading: {[key: string]: boolean}) => ({...prevLoading, [provider]: false}));
|
||||||
|
|
@ -423,7 +424,7 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (type === 'series' && episodeId) {
|
if (type === 'series' && episodeId) {
|
||||||
console.log(`🎬 Loading episode streams for: ${episodeId}`);
|
logger.log(`🎬 Loading episode streams for: ${episodeId}`);
|
||||||
setLoadingProviders({
|
setLoadingProviders({
|
||||||
'source_1': true,
|
'source_1': true,
|
||||||
'source_2': true,
|
'source_2': true,
|
||||||
|
|
@ -432,7 +433,7 @@ export const StreamsScreen = () => {
|
||||||
setSelectedEpisode(episodeId);
|
setSelectedEpisode(episodeId);
|
||||||
loadEpisodeStreams(episodeId);
|
loadEpisodeStreams(episodeId);
|
||||||
} else if (type === 'movie') {
|
} else if (type === 'movie') {
|
||||||
console.log(`🎬 Loading movie streams for: ${id}`);
|
logger.log(`🎬 Loading movie streams for: ${id}`);
|
||||||
setLoadingProviders({
|
setLoadingProviders({
|
||||||
'source_1': true,
|
'source_1': true,
|
||||||
'source_2': true,
|
'source_2': true,
|
||||||
|
|
@ -506,7 +507,7 @@ export const StreamsScreen = () => {
|
||||||
const handleStreamPress = useCallback(async (stream: Stream) => {
|
const handleStreamPress = useCallback(async (stream: Stream) => {
|
||||||
try {
|
try {
|
||||||
if (stream.url) {
|
if (stream.url) {
|
||||||
console.log('handleStreamPress called with stream:', {
|
logger.log('handleStreamPress called with stream:', {
|
||||||
url: stream.url,
|
url: stream.url,
|
||||||
behaviorHints: stream.behaviorHints,
|
behaviorHints: stream.behaviorHints,
|
||||||
isMagnet: stream.url.startsWith('magnet:'),
|
isMagnet: stream.url.startsWith('magnet:'),
|
||||||
|
|
@ -518,7 +519,7 @@ export const StreamsScreen = () => {
|
||||||
const isMagnet = stream.url.startsWith('magnet:') || stream.behaviorHints?.isMagnetStream;
|
const isMagnet = stream.url.startsWith('magnet:') || stream.behaviorHints?.isMagnetStream;
|
||||||
|
|
||||||
if (isMagnet) {
|
if (isMagnet) {
|
||||||
console.log('Handling magnet link...');
|
logger.log('Handling magnet link...');
|
||||||
// Check if there's already an active torrent
|
// Check if there's already an active torrent
|
||||||
if (activeTorrent && activeTorrent !== stream.url) {
|
if (activeTorrent && activeTorrent !== stream.url) {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
|
|
@ -533,7 +534,7 @@ export const StreamsScreen = () => {
|
||||||
text: 'Stop and Switch',
|
text: 'Stop and Switch',
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
console.log('Stopping current torrent and starting new one');
|
logger.log('Stopping current torrent and starting new one');
|
||||||
await torrentService.stopStreamAndWait();
|
await torrentService.stopStreamAndWait();
|
||||||
setActiveTorrent(null);
|
setActiveTorrent(null);
|
||||||
setTorrentProgress({});
|
setTorrentProgress({});
|
||||||
|
|
@ -545,14 +546,14 @@ export const StreamsScreen = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Starting torrent stream...');
|
logger.log('Starting torrent stream...');
|
||||||
startTorrentStream(stream);
|
startTorrentStream(stream);
|
||||||
} else {
|
} else {
|
||||||
console.log('Playing regular stream...');
|
logger.log('Playing regular stream...');
|
||||||
|
|
||||||
// Check if external player is enabled in settings
|
// Check if external player is enabled in settings
|
||||||
if (settings.useExternalPlayer) {
|
if (settings.useExternalPlayer) {
|
||||||
console.log('Using external player for URL:', stream.url);
|
logger.log('Using external player for URL:', stream.url);
|
||||||
// Use VideoPlayerService to launch external player
|
// Use VideoPlayerService to launch external player
|
||||||
try {
|
try {
|
||||||
const videoPlayerService = VideoPlayerService;
|
const videoPlayerService = VideoPlayerService;
|
||||||
|
|
@ -564,7 +565,7 @@ export const StreamsScreen = () => {
|
||||||
releaseDate: metadata?.year?.toString(),
|
releaseDate: metadata?.year?.toString(),
|
||||||
});
|
});
|
||||||
} catch (externalPlayerError) {
|
} catch (externalPlayerError) {
|
||||||
console.error('External player error:', externalPlayerError);
|
logger.error('External player error:', externalPlayerError);
|
||||||
// Fallback to built-in player if external player fails
|
// Fallback to built-in player if external player fails
|
||||||
navigation.navigate('Player', {
|
navigation.navigate('Player', {
|
||||||
uri: stream.url,
|
uri: stream.url,
|
||||||
|
|
@ -599,7 +600,7 @@ export const StreamsScreen = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Stream error:', error);
|
logger.error('Stream error:', error);
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Playback Error',
|
'Playback Error',
|
||||||
error instanceof Error ? error.message : 'An error occurred while playing the video'
|
error instanceof Error ? error.message : 'An error occurred while playing the video'
|
||||||
|
|
@ -611,16 +612,16 @@ export const StreamsScreen = () => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const unsubscribe = navigation.addListener('focus', () => {
|
const unsubscribe = navigation.addListener('focus', () => {
|
||||||
// This runs when returning from the player screen
|
// This runs when returning from the player screen
|
||||||
console.log('[StreamsScreen] Screen focused, checking if cleanup needed');
|
logger.log('[StreamsScreen] Screen focused, checking if cleanup needed');
|
||||||
if (isVideoPlaying) {
|
if (isVideoPlaying) {
|
||||||
console.log('[StreamsScreen] Playback ended, cleaning up torrent');
|
logger.log('[StreamsScreen] Playback ended, cleaning up torrent');
|
||||||
setIsVideoPlaying(false);
|
setIsVideoPlaying(false);
|
||||||
|
|
||||||
// Clean up the torrent when returning from video player
|
// Clean up the torrent when returning from video player
|
||||||
if (activeTorrent) {
|
if (activeTorrent) {
|
||||||
console.log('[StreamsScreen] Stopping torrent after playback');
|
logger.log('[StreamsScreen] Stopping torrent after playback');
|
||||||
torrentService.stopStreamAndWait().catch(error => {
|
torrentService.stopStreamAndWait().catch(error => {
|
||||||
console.error('[StreamsScreen] Error during cleanup:', error);
|
logger.error('[StreamsScreen] Error during cleanup:', error);
|
||||||
});
|
});
|
||||||
setActiveTorrent(null);
|
setActiveTorrent(null);
|
||||||
setTorrentProgress({});
|
setTorrentProgress({});
|
||||||
|
|
@ -630,11 +631,11 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
console.log('[StreamsScreen] Component unmounting, cleaning up torrent');
|
logger.log('[StreamsScreen] Component unmounting, cleaning up torrent');
|
||||||
if (activeTorrent) {
|
if (activeTorrent) {
|
||||||
console.log('[StreamsScreen] Stopping torrent on unmount');
|
logger.log('[StreamsScreen] Stopping torrent on unmount');
|
||||||
torrentService.stopStreamAndWait().catch(error => {
|
torrentService.stopStreamAndWait().catch(error => {
|
||||||
console.error('[StreamsScreen] Error during cleanup:', error);
|
logger.error('[StreamsScreen] Error during cleanup:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -644,7 +645,7 @@ export const StreamsScreen = () => {
|
||||||
if (!stream.url) return;
|
if (!stream.url) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[StreamsScreen] Starting torrent stream with URL:', stream.url);
|
logger.log('[StreamsScreen] Starting torrent stream with URL:', stream.url);
|
||||||
|
|
||||||
// Make sure any existing stream is fully stopped
|
// Make sure any existing stream is fully stopped
|
||||||
if (activeTorrent && activeTorrent !== stream.url) {
|
if (activeTorrent && activeTorrent !== stream.url) {
|
||||||
|
|
@ -660,11 +661,11 @@ export const StreamsScreen = () => {
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
// Check if progress object is valid and has data
|
// Check if progress object is valid and has data
|
||||||
if (!progress || Object.keys(progress).length === 0) {
|
if (!progress || Object.keys(progress).length === 0) {
|
||||||
console.log('[StreamsScreen] Received empty progress object, ignoring');
|
logger.log('[StreamsScreen] Received empty progress object, ignoring');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[StreamsScreen] Torrent progress update:', {
|
logger.log('[StreamsScreen] Torrent progress update:', {
|
||||||
url: stream.url,
|
url: stream.url,
|
||||||
progress,
|
progress,
|
||||||
currentTorrentProgress: torrentProgress[stream.url!]
|
currentTorrentProgress: torrentProgress[stream.url!]
|
||||||
|
|
@ -684,7 +685,7 @@ export const StreamsScreen = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[StreamsScreen] Got video path:', videoPath);
|
logger.log('[StreamsScreen] Got video path:', videoPath);
|
||||||
|
|
||||||
// Once we have the video file path, play it using VideoPlayer screen
|
// Once we have the video file path, play it using VideoPlayer screen
|
||||||
if (videoPath) {
|
if (videoPath) {
|
||||||
|
|
@ -692,7 +693,7 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (settings.useExternalPlayer) {
|
if (settings.useExternalPlayer) {
|
||||||
console.log('[StreamsScreen] Using external player for torrent video path:', videoPath);
|
logger.log('[StreamsScreen] Using external player for torrent video path:', videoPath);
|
||||||
// Use VideoPlayerService to launch external player
|
// Use VideoPlayerService to launch external player
|
||||||
try {
|
try {
|
||||||
const videoPlayerService = VideoPlayerService;
|
const videoPlayerService = VideoPlayerService;
|
||||||
|
|
@ -704,7 +705,7 @@ export const StreamsScreen = () => {
|
||||||
releaseDate: metadata?.year?.toString(),
|
releaseDate: metadata?.year?.toString(),
|
||||||
});
|
});
|
||||||
} catch (externalPlayerError) {
|
} catch (externalPlayerError) {
|
||||||
console.error('[StreamsScreen] External player error:', externalPlayerError);
|
logger.error('[StreamsScreen] External player error:', externalPlayerError);
|
||||||
// Fallback to built-in player if external player fails
|
// Fallback to built-in player if external player fails
|
||||||
navigation.navigate('Player', {
|
navigation.navigate('Player', {
|
||||||
uri: `file://${videoPath}`,
|
uri: `file://${videoPath}`,
|
||||||
|
|
@ -735,11 +736,11 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
// Note: Cleanup happens in the focus effect when returning from the player
|
// Note: Cleanup happens in the focus effect when returning from the player
|
||||||
} catch (playerError) {
|
} catch (playerError) {
|
||||||
console.error('[StreamsScreen] Video player navigation error:', playerError);
|
logger.error('[StreamsScreen] Video player navigation error:', playerError);
|
||||||
setIsVideoPlaying(false);
|
setIsVideoPlaying(false);
|
||||||
|
|
||||||
// Also stop the torrent on player error
|
// Also stop the torrent on player error
|
||||||
console.log('[StreamsScreen] Stopping torrent after player error');
|
logger.log('[StreamsScreen] Stopping torrent after player error');
|
||||||
await torrentService.stopStreamAndWait();
|
await torrentService.stopStreamAndWait();
|
||||||
setActiveTorrent(null);
|
setActiveTorrent(null);
|
||||||
setTorrentProgress({});
|
setTorrentProgress({});
|
||||||
|
|
@ -748,7 +749,7 @@ export const StreamsScreen = () => {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If we didn't get a video path, there's a problem
|
// If we didn't get a video path, there's a problem
|
||||||
console.error('[StreamsScreen] No video path returned from torrent service');
|
logger.error('[StreamsScreen] No video path returned from torrent service');
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Playback Error',
|
'Playback Error',
|
||||||
'No video file found in torrent'
|
'No video file found in torrent'
|
||||||
|
|
@ -759,7 +760,7 @@ export const StreamsScreen = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[StreamsScreen] Torrent error:', error);
|
logger.error('[StreamsScreen] Torrent error:', error);
|
||||||
// Clean up on error
|
// Clean up on error
|
||||||
setIsVideoPlaying(false);
|
setIsVideoPlaying(false);
|
||||||
await torrentService.stopStreamAndWait();
|
await torrentService.stopStreamAndWait();
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { storageService } from '../services/storageService';
|
import { storageService } from '../services/storageService';
|
||||||
// Add throttle/debounce imports
|
// Add throttle/debounce imports
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
// Define the TrackPreferenceType for audio/text tracks
|
// Define the TrackPreferenceType for audio/text tracks
|
||||||
type TrackPreferenceType = 'system' | 'disabled' | 'title' | 'language' | 'index';
|
type TrackPreferenceType = 'system' | 'disabled' | 'title' | 'language' | 'index';
|
||||||
|
|
@ -87,7 +88,7 @@ const VideoPlayer = () => {
|
||||||
const uri = __DEV__ && (!routeUri || routeUri.trim() === '') ? developmentTestUrl : routeUri;
|
const uri = __DEV__ && (!routeUri || routeUri.trim() === '') ? developmentTestUrl : routeUri;
|
||||||
|
|
||||||
// Log received props for debugging
|
// Log received props for debugging
|
||||||
console.log("VideoPlayer received route params:", {
|
logger.log("VideoPlayer received route params:", {
|
||||||
uri,
|
uri,
|
||||||
title,
|
title,
|
||||||
season,
|
season,
|
||||||
|
|
@ -104,10 +105,10 @@ const VideoPlayer = () => {
|
||||||
// Validate URI
|
// Validate URI
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
console.error("Empty or null URI received in VideoPlayer");
|
logger.error("Empty or null URI received in VideoPlayer");
|
||||||
alert("Error: No video URL provided");
|
alert("Error: No video URL provided");
|
||||||
} else {
|
} else {
|
||||||
console.log("Video URI:", uri);
|
logger.log("Video URI:", uri);
|
||||||
}
|
}
|
||||||
}, [uri]);
|
}, [uri]);
|
||||||
|
|
||||||
|
|
@ -249,7 +250,7 @@ const VideoPlayer = () => {
|
||||||
ScreenOrientation.OrientationLock.LANDSCAPE
|
ScreenOrientation.OrientationLock.LANDSCAPE
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to lock orientation:", error);
|
logger.error("Failed to lock orientation:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -258,7 +259,7 @@ const VideoPlayer = () => {
|
||||||
try {
|
try {
|
||||||
await ScreenOrientation.unlockAsync();
|
await ScreenOrientation.unlockAsync();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to unlock orientation:", error);
|
logger.error("Failed to unlock orientation:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -478,7 +479,7 @@ const VideoPlayer = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTextTracks = (e: Readonly<{ textTracks: TextTrack[] }>) => {
|
const onTextTracks = (e: Readonly<{ textTracks: TextTrack[] }>) => {
|
||||||
console.log("Detected Text Tracks:", e.textTracks);
|
logger.log("Detected Text Tracks:", e.textTracks);
|
||||||
setTextTracks(e.textTracks || []);
|
setTextTracks(e.textTracks || []);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -486,13 +487,13 @@ const VideoPlayer = () => {
|
||||||
const cycleAspectRatio = () => {
|
const cycleAspectRatio = () => {
|
||||||
const currentIndex = resizeModes.indexOf(resizeMode);
|
const currentIndex = resizeModes.indexOf(resizeMode);
|
||||||
const nextIndex = (currentIndex + 1) % resizeModes.length;
|
const nextIndex = (currentIndex + 1) % resizeModes.length;
|
||||||
console.log(`Changing aspect ratio from ${resizeMode} to ${resizeModes[nextIndex]}`);
|
logger.log(`Changing aspect ratio from ${resizeMode} to ${resizeModes[nextIndex]}`);
|
||||||
setResizeMode(resizeModes[nextIndex]);
|
setResizeMode(resizeModes[nextIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function for Back button
|
// Function for Back button
|
||||||
const handleBackPress = () => {
|
const handleBackPress = () => {
|
||||||
console.log("Close button pressed");
|
logger.log("Close button pressed");
|
||||||
|
|
||||||
// Pause video before leaving
|
// Pause video before leaving
|
||||||
setPaused(true);
|
setPaused(true);
|
||||||
|
|
@ -515,7 +516,7 @@ const VideoPlayer = () => {
|
||||||
}, 350); // Increase delay to ensure orientation reset completes
|
}, 350); // Increase delay to ensure orientation reset completes
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Error resetting orientation:", error);
|
logger.error("Error resetting orientation:", error);
|
||||||
// Navigate back anyway after a short delay
|
// Navigate back anyway after a short delay
|
||||||
disableImmersiveMode(); // Try disabling again
|
disableImmersiveMode(); // Try disabling again
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -683,11 +684,11 @@ const VideoPlayer = () => {
|
||||||
renderToHardwareTextureAndroid={true}
|
renderToHardwareTextureAndroid={true}
|
||||||
|
|
||||||
onBuffer={(buffer) => {
|
onBuffer={(buffer) => {
|
||||||
console.log('Buffering:', buffer.isBuffering);
|
logger.log('Buffering:', buffer.isBuffering);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
onError={(error) => {
|
onError={(error) => {
|
||||||
console.error('Video playback error:', error);
|
logger.error('Video playback error:', error);
|
||||||
alert(`Video Error: ${error.error.errorString} (Code: ${error.error.errorCode})`);
|
alert(`Video Error: ${error.error.errorString} (Code: ${error.error.errorCode})`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,8 @@ class CacheService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public cacheMetadataScreen(id: string, type: string, data: any) {
|
public cacheMetadataScreen(id: string, type: string, data: any) {
|
||||||
|
if (!id || !type) return;
|
||||||
|
|
||||||
const key = `${type}:${id}`;
|
const key = `${type}:${id}`;
|
||||||
|
|
||||||
// If this item is already in cache, just update it
|
// If this item is already in cache, just update it
|
||||||
|
|
@ -141,7 +143,9 @@ class CacheService {
|
||||||
// If we've reached the limit, remove the oldest item
|
// If we've reached the limit, remove the oldest item
|
||||||
if (this.metadataScreenCache.size >= this.MAX_METADATA_SCREENS) {
|
if (this.metadataScreenCache.size >= this.MAX_METADATA_SCREENS) {
|
||||||
const firstKey = this.metadataScreenCache.keys().next().value;
|
const firstKey = this.metadataScreenCache.keys().next().value;
|
||||||
this.metadataScreenCache.delete(firstKey);
|
if (firstKey) {
|
||||||
|
this.metadataScreenCache.delete(firstKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new item
|
// Add the new item
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { stremioService, Meta, Manifest } from './stremioService';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { TMDBService } from './tmdbService';
|
import { TMDBService } from './tmdbService';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
export interface StreamingAddon {
|
export interface StreamingAddon {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -82,7 +83,7 @@ class CatalogService {
|
||||||
this.library = JSON.parse(storedLibrary);
|
this.library = JSON.parse(storedLibrary);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load library:', error);
|
logger.error('Failed to load library:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,7 +91,7 @@ class CatalogService {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(this.LIBRARY_KEY, JSON.stringify(this.library));
|
await AsyncStorage.setItem(this.LIBRARY_KEY, JSON.stringify(this.library));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save library:', error);
|
logger.error('Failed to save library:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +102,7 @@ class CatalogService {
|
||||||
this.recentContent = JSON.parse(storedRecentContent);
|
this.recentContent = JSON.parse(storedRecentContent);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load recent content:', error);
|
logger.error('Failed to load recent content:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +110,7 @@ class CatalogService {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(this.RECENT_CONTENT_KEY, JSON.stringify(this.recentContent));
|
await AsyncStorage.setItem(this.RECENT_CONTENT_KEY, JSON.stringify(this.recentContent));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save recent content:', error);
|
logger.error('Failed to save recent content:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,7 +194,7 @@ class CatalogService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get catalog ${catalog.id} for addon ${addon.id}:`, error);
|
logger.error(`Failed to get catalog ${catalog.id} for addon ${addon.id}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +239,7 @@ class CatalogService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get catalog ${catalog.id} for addon ${addon.id}:`, error);
|
logger.error(`Failed to get catalog ${catalog.id} for addon ${addon.id}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +260,7 @@ class CatalogService {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
|
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
console.error(`Attempt ${i + 1} failed to get content details for ${type}:${id}:`, error);
|
logger.error(`Attempt ${i + 1} failed to get content details for ${type}:${id}:`, error);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
|
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +282,7 @@ class CatalogService {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get content details for ${type}:${id}:`, error);
|
logger.error(`Failed to get content details for ${type}:${id}:`, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -389,7 +390,7 @@ class CatalogService {
|
||||||
results.push(...items);
|
results.push(...items);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Search failed for ${catalog.id} in addon ${addon.id}:`, error);
|
logger.error(`Search failed for ${catalog.id} in addon ${addon.id}:`, error);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
@ -414,7 +415,7 @@ class CatalogService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimmedQuery = query.trim().toLowerCase();
|
const trimmedQuery = query.trim().toLowerCase();
|
||||||
console.log('Searching Cinemeta for:', trimmedQuery);
|
logger.log('Searching Cinemeta for:', trimmedQuery);
|
||||||
|
|
||||||
const addons = await this.getAllAddons();
|
const addons = await this.getAllAddons();
|
||||||
const results: StreamingContent[] = [];
|
const results: StreamingContent[] = [];
|
||||||
|
|
@ -423,7 +424,7 @@ class CatalogService {
|
||||||
const cinemeta = addons.find(addon => addon.id === 'com.linvo.cinemeta');
|
const cinemeta = addons.find(addon => addon.id === 'com.linvo.cinemeta');
|
||||||
|
|
||||||
if (!cinemeta || !cinemeta.catalogs) {
|
if (!cinemeta || !cinemeta.catalogs) {
|
||||||
console.error('Cinemeta addon not found');
|
logger.error('Cinemeta addon not found');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,7 +433,7 @@ class CatalogService {
|
||||||
try {
|
try {
|
||||||
// Direct API call to Cinemeta
|
// Direct API call to Cinemeta
|
||||||
const url = `https://v3-cinemeta.strem.io/catalog/${type}/top/search=${encodeURIComponent(trimmedQuery)}.json`;
|
const url = `https://v3-cinemeta.strem.io/catalog/${type}/top/search=${encodeURIComponent(trimmedQuery)}.json`;
|
||||||
console.log('Request URL:', url);
|
logger.log('Request URL:', url);
|
||||||
|
|
||||||
const response = await axios.get<{ metas: any[] }>(url);
|
const response = await axios.get<{ metas: any[] }>(url);
|
||||||
const metas = response.data.metas || [];
|
const metas = response.data.metas || [];
|
||||||
|
|
@ -442,7 +443,7 @@ class CatalogService {
|
||||||
results.push(...items);
|
results.push(...items);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Cinemeta search failed for ${type}:`, error);
|
logger.error(`Cinemeta search failed for ${type}:`, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -474,7 +475,7 @@ class CatalogService {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting Stremio ID:', error);
|
logger.error('Error getting Stremio ID:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Platform } from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { parseISO, differenceInHours, isToday, addDays } from 'date-fns';
|
import { parseISO, differenceInHours, isToday, addDays } from 'date-fns';
|
||||||
import { stremioService } from './stremioService';
|
import { stremioService } from './stremioService';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
// Define notification storage keys
|
// Define notification storage keys
|
||||||
const NOTIFICATION_STORAGE_KEY = 'stremio-notifications';
|
const NOTIFICATION_STORAGE_KEY = 'stremio-notifications';
|
||||||
|
|
@ -92,7 +93,7 @@ class NotificationService {
|
||||||
this.settings = { ...DEFAULT_NOTIFICATION_SETTINGS, ...JSON.parse(storedSettings) };
|
this.settings = { ...DEFAULT_NOTIFICATION_SETTINGS, ...JSON.parse(storedSettings) };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading notification settings:', error);
|
logger.error('Error loading notification settings:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,7 +101,7 @@ class NotificationService {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(NOTIFICATION_SETTINGS_KEY, JSON.stringify(this.settings));
|
await AsyncStorage.setItem(NOTIFICATION_SETTINGS_KEY, JSON.stringify(this.settings));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving notification settings:', error);
|
logger.error('Error saving notification settings:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,7 +113,7 @@ class NotificationService {
|
||||||
this.scheduledNotifications = JSON.parse(storedNotifications);
|
this.scheduledNotifications = JSON.parse(storedNotifications);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading scheduled notifications:', error);
|
logger.error('Error loading scheduled notifications:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,7 +121,7 @@ class NotificationService {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(NOTIFICATION_STORAGE_KEY, JSON.stringify(this.scheduledNotifications));
|
await AsyncStorage.setItem(NOTIFICATION_STORAGE_KEY, JSON.stringify(this.scheduledNotifications));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving scheduled notifications:', error);
|
logger.error('Error saving scheduled notifications:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,7 +185,7 @@ class NotificationService {
|
||||||
|
|
||||||
return notificationId;
|
return notificationId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error scheduling notification:', error);
|
logger.error('Error scheduling notification:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -219,7 +220,7 @@ class NotificationService {
|
||||||
// Save updated list
|
// Save updated list
|
||||||
await this.saveScheduledNotifications();
|
await this.saveScheduledNotifications();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error canceling notification:', error);
|
logger.error('Error canceling notification:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,7 +230,7 @@ class NotificationService {
|
||||||
this.scheduledNotifications = [];
|
this.scheduledNotifications = [];
|
||||||
await this.saveScheduledNotifications();
|
await this.saveScheduledNotifications();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error canceling all notifications:', error);
|
logger.error('Error canceling all notifications:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,7 +278,7 @@ class NotificationService {
|
||||||
|
|
||||||
await this.scheduleMultipleEpisodeNotifications(notificationItems);
|
await this.scheduleMultipleEpisodeNotifications(notificationItems);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error updating notifications for series ${seriesId}:`, error);
|
logger.error(`Error updating notifications for series ${seriesId}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
interface WatchProgress {
|
interface WatchProgress {
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
|
|
@ -33,7 +34,7 @@ class StorageService {
|
||||||
const key = this.getWatchProgressKey(id, type, episodeId);
|
const key = this.getWatchProgressKey(id, type, episodeId);
|
||||||
await AsyncStorage.setItem(key, JSON.stringify(progress));
|
await AsyncStorage.setItem(key, JSON.stringify(progress));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving watch progress:', error);
|
logger.error('Error saving watch progress:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,7 +48,7 @@ class StorageService {
|
||||||
const data = await AsyncStorage.getItem(key);
|
const data = await AsyncStorage.getItem(key);
|
||||||
return data ? JSON.parse(data) : null;
|
return data ? JSON.parse(data) : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting watch progress:', error);
|
logger.error('Error getting watch progress:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +62,7 @@ class StorageService {
|
||||||
const key = this.getWatchProgressKey(id, type, episodeId);
|
const key = this.getWatchProgressKey(id, type, episodeId);
|
||||||
await AsyncStorage.removeItem(key);
|
await AsyncStorage.removeItem(key);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error removing watch progress:', error);
|
logger.error('Error removing watch progress:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +78,7 @@ class StorageService {
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, WatchProgress>);
|
}, {} as Record<string, WatchProgress>);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting all watch progress:', error);
|
logger.error('Error getting all watch progress:', error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
// Basic types for Stremio
|
// Basic types for Stremio
|
||||||
export interface Meta {
|
export interface Meta {
|
||||||
|
|
@ -163,7 +164,7 @@ class StremioService {
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize addons:', error);
|
logger.error('Failed to initialize addons:', error);
|
||||||
// Install defaults as fallback
|
// Install defaults as fallback
|
||||||
await this.installDefaultAddons();
|
await this.installDefaultAddons();
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
|
|
@ -184,7 +185,7 @@ class StremioService {
|
||||||
return await request();
|
return await request();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
console.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}):`, {
|
logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}):`, {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
code: error.code,
|
code: error.code,
|
||||||
isAxiosError: error.isAxiosError,
|
isAxiosError: error.isAxiosError,
|
||||||
|
|
@ -193,7 +194,7 @@ class StremioService {
|
||||||
|
|
||||||
if (attempt < retries) {
|
if (attempt < retries) {
|
||||||
const backoffDelay = delay * Math.pow(2, attempt);
|
const backoffDelay = delay * Math.pow(2, attempt);
|
||||||
console.log(`Retrying in ${backoffDelay}ms...`);
|
logger.log(`Retrying in ${backoffDelay}ms...`);
|
||||||
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,7 +212,7 @@ class StremioService {
|
||||||
}
|
}
|
||||||
await this.saveInstalledAddons();
|
await this.saveInstalledAddons();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to install default addons:', error);
|
logger.error('Failed to install default addons:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +221,7 @@ class StremioService {
|
||||||
const addonsArray = Array.from(this.installedAddons.values());
|
const addonsArray = Array.from(this.installedAddons.values());
|
||||||
await AsyncStorage.setItem(this.STORAGE_KEY, JSON.stringify(addonsArray));
|
await AsyncStorage.setItem(this.STORAGE_KEY, JSON.stringify(addonsArray));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save addons:', error);
|
logger.error('Failed to save addons:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,7 +249,7 @@ class StremioService {
|
||||||
|
|
||||||
return manifest;
|
return manifest;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to fetch manifest from ${url}:`, error);
|
logger.error(`Failed to fetch manifest from ${url}:`, error);
|
||||||
throw new Error(`Failed to fetch addon manifest from ${url}`);
|
throw new Error(`Failed to fetch addon manifest from ${url}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -298,7 +299,7 @@ class StremioService {
|
||||||
result[addon.id] = items;
|
result[addon.id] = items;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to fetch catalog from ${addon.name}:`, error);
|
logger.error(`Failed to fetch catalog from ${addon.name}:`, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -315,7 +316,7 @@ class StremioService {
|
||||||
baseUrl = `https://${baseUrl}`;
|
baseUrl = `https://${baseUrl}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Addon base URL:', baseUrl);
|
logger.log('Addon base URL:', baseUrl);
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,7 +380,7 @@ class StremioService {
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to fetch catalog from ${manifest.name}:`, error);
|
logger.error(`Failed to fetch catalog from ${manifest.name}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -403,7 +404,7 @@ class StremioService {
|
||||||
return response.data.meta;
|
return response.data.meta;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to fetch meta from ${baseUrl}:`, error);
|
logger.warn(`Failed to fetch meta from ${baseUrl}:`, error);
|
||||||
continue; // Try next URL
|
continue; // Try next URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -432,15 +433,15 @@ class StremioService {
|
||||||
return response.data.meta;
|
return response.data.meta;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to fetch meta from ${addon.name}:`, error);
|
logger.warn(`Failed to fetch meta from ${addon.name}:`, error);
|
||||||
continue; // Try next addon
|
continue; // Try next addon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn('No metadata found from any addon');
|
logger.warn('No metadata found from any addon');
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getMetaDetails:', error);
|
logger.error('Error in getMetaDetails:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -449,7 +450,7 @@ class StremioService {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
const addons = this.getInstalledAddons();
|
const addons = this.getInstalledAddons();
|
||||||
console.log('Installed addons:', addons.map(a => ({ id: a.id, url: a.url })));
|
logger.log('Installed addons:', addons.map(a => ({ id: a.id, url: a.url })));
|
||||||
|
|
||||||
const streamResponses: StreamResponse[] = [];
|
const streamResponses: StreamResponse[] = [];
|
||||||
|
|
||||||
|
|
@ -457,7 +458,7 @@ class StremioService {
|
||||||
const streamAddons = addons
|
const streamAddons = addons
|
||||||
.filter(addon => {
|
.filter(addon => {
|
||||||
if (!addon.resources) {
|
if (!addon.resources) {
|
||||||
console.log(`Addon ${addon.id} has no resources`);
|
logger.log(`Addon ${addon.id} has no resources`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -466,16 +467,16 @@ class StremioService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasStreamResource) {
|
if (!hasStreamResource) {
|
||||||
console.log(`Addon ${addon.id} does not support streaming ${type}`);
|
logger.log(`Addon ${addon.id} does not support streaming ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasStreamResource;
|
return hasStreamResource;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Stream capable addons:', streamAddons.map(a => a.id));
|
logger.log('Stream capable addons:', streamAddons.map(a => a.id));
|
||||||
|
|
||||||
if (streamAddons.length === 0) {
|
if (streamAddons.length === 0) {
|
||||||
console.warn('No addons found that can provide streams');
|
logger.warn('No addons found that can provide streams');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -487,7 +488,7 @@ class StremioService {
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
try {
|
try {
|
||||||
if (!addon.url) {
|
if (!addon.url) {
|
||||||
console.warn(`Addon ${addon.id} has no URL`);
|
logger.warn(`Addon ${addon.id} has no URL`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -513,7 +514,7 @@ class StremioService {
|
||||||
callback(response.data?.streams || null, addon.name, null);
|
callback(response.data?.streams || null, addon.name, null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get streams from ${addon.name}:`, error);
|
logger.error(`Failed to get streams from ${addon.name}:`, error);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(null, addon.name, error as Error);
|
callback(null, addon.name, error as Error);
|
||||||
}
|
}
|
||||||
|
|
@ -538,21 +539,21 @@ class StremioService {
|
||||||
|
|
||||||
private async fetchStreamsFromAddon(addon: Manifest, type: string, id: string): Promise<StreamResponse | null> {
|
private async fetchStreamsFromAddon(addon: Manifest, type: string, id: string): Promise<StreamResponse | null> {
|
||||||
if (!addon.url) {
|
if (!addon.url) {
|
||||||
console.warn(`Addon ${addon.id} has no URL defined`);
|
logger.warn(`Addon ${addon.id} has no URL defined`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = this.getAddonBaseURL(addon.url);
|
const baseUrl = this.getAddonBaseURL(addon.url);
|
||||||
const url = `${baseUrl}/stream/${type}/${id}.json`;
|
const url = `${baseUrl}/stream/${type}/${id}.json`;
|
||||||
|
|
||||||
console.log(`Fetching streams from URL: ${url}`);
|
logger.log(`Fetching streams from URL: ${url}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Increase timeout for debrid services
|
// Increase timeout for debrid services
|
||||||
const timeout = addon.id.toLowerCase().includes('torrentio') ? 30000 : 10000;
|
const timeout = addon.id.toLowerCase().includes('torrentio') ? 30000 : 10000;
|
||||||
|
|
||||||
const response = await this.retryRequest(async () => {
|
const response = await this.retryRequest(async () => {
|
||||||
console.log(`Making request to ${url} with timeout ${timeout}ms`);
|
logger.log(`Making request to ${url} with timeout ${timeout}ms`);
|
||||||
return await axios.get(url, {
|
return await axios.get(url, {
|
||||||
timeout,
|
timeout,
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -564,7 +565,7 @@ class StremioService {
|
||||||
|
|
||||||
if (response.data && response.data.streams && Array.isArray(response.data.streams)) {
|
if (response.data && response.data.streams && Array.isArray(response.data.streams)) {
|
||||||
const streams = this.processStreams(response.data.streams, addon);
|
const streams = this.processStreams(response.data.streams, addon);
|
||||||
console.log(`Successfully processed ${streams.length} streams from ${addon.id}`);
|
logger.log(`Successfully processed ${streams.length} streams from ${addon.id}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
streams,
|
streams,
|
||||||
|
|
@ -572,7 +573,7 @@ class StremioService {
|
||||||
addonName: addon.name
|
addonName: addon.name
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Invalid response format from ${addon.id}:`, response.data);
|
logger.warn(`Invalid response format from ${addon.id}:`, response.data);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorDetails = {
|
const errorDetails = {
|
||||||
|
|
@ -585,7 +586,7 @@ class StremioService {
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
responseData: error.response?.data
|
responseData: error.response?.data
|
||||||
};
|
};
|
||||||
console.error('Failed to fetch streams from addon:', errorDetails);
|
logger.error('Failed to fetch streams from addon:', errorDetails);
|
||||||
|
|
||||||
// Re-throw the error with more context
|
// Re-throw the error with more context
|
||||||
throw new Error(`Failed to fetch streams from ${addon.name}: ${error.message}`);
|
throw new Error(`Failed to fetch streams from ${addon.name}: ${error.message}`);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
// TMDB API configuration
|
// TMDB API configuration
|
||||||
const API_KEY = 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0MzljNDc4YTc3MWYzNWMwNTAyMmY5ZmVhYmNjYTAxYyIsIm5iZiI6MTcwOTkxMTEzNS4xNCwic3ViIjoiNjVlYjJjNWYzODlkYTEwMTYyZDgyOWU0Iiwic2NvcGVzIjpbImFwaV9yZWFkIl0sInZlcnNpb24iOjF9.gosBVl1wYUbePOeB9WieHn8bY9x938-GSGmlXZK_UVM';
|
const API_KEY = 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0MzljNDc4YTc3MWYzNWMwNTAyMmY5ZmVhYmNjYTAxYyIsIm5iZiI6MTcwOTkxMTEzNS4xNCwic3ViIjoiNjVlYjJjNWYzODlkYTEwMTYyZDgyOWU0Iiwic2NvcGVzIjpbImFwaV9yZWFkIl0sInZlcnNpb24iOjF9.gosBVl1wYUbePOeB9WieHn8bY9x938-GSGmlXZK_UVM';
|
||||||
|
|
@ -49,6 +50,22 @@ export interface TMDBShow {
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TMDBTrendingResult {
|
||||||
|
id: number;
|
||||||
|
title?: string;
|
||||||
|
name?: string;
|
||||||
|
overview: string;
|
||||||
|
poster_path: string | null;
|
||||||
|
backdrop_path: string | null;
|
||||||
|
release_date?: string;
|
||||||
|
first_air_date?: string;
|
||||||
|
genre_ids: number[];
|
||||||
|
external_ids?: {
|
||||||
|
imdb_id: string | null;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class TMDBService {
|
export class TMDBService {
|
||||||
private static instance: TMDBService;
|
private static instance: TMDBService;
|
||||||
private static ratingCache: Map<string, number | null> = new Map();
|
private static ratingCache: Map<string, number | null> = new Map();
|
||||||
|
|
@ -89,7 +106,7 @@ export class TMDBService {
|
||||||
});
|
});
|
||||||
return response.data.results;
|
return response.data.results;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to search TV show:', error);
|
logger.error('Failed to search TV show:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +124,7 @@ export class TMDBService {
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get TV show details:', error);
|
logger.error('Failed to get TV show details:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +146,7 @@ export class TMDBService {
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get episode external IDs:', error);
|
logger.error('Failed to get episode external IDs:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,7 +182,7 @@ export class TMDBService {
|
||||||
TMDBService.ratingCache.set(cacheKey, rating);
|
TMDBService.ratingCache.set(cacheKey, rating);
|
||||||
return rating;
|
return rating;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get IMDb rating:', error);
|
logger.error('Failed to get IMDb rating:', error);
|
||||||
// Cache the failed result too to prevent repeated failed requests
|
// Cache the failed result too to prevent repeated failed requests
|
||||||
TMDBService.ratingCache.set(cacheKey, null);
|
TMDBService.ratingCache.set(cacheKey, null);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -220,7 +237,7 @@ export class TMDBService {
|
||||||
|
|
||||||
return season;
|
return season;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get season details:', error);
|
logger.error('Failed to get season details:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +262,7 @@ export class TMDBService {
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get episode details:', error);
|
logger.error('Failed to get episode details:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +281,7 @@ export class TMDBService {
|
||||||
const tmdbId = await this.findTMDBIdByIMDB(imdbId);
|
const tmdbId = await this.findTMDBIdByIMDB(imdbId);
|
||||||
return tmdbId;
|
return tmdbId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to extract TMDB ID from Stremio ID:', error);
|
logger.error('Failed to extract TMDB ID from Stremio ID:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -297,7 +314,7 @@ export class TMDBService {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to find TMDB ID by IMDB ID:', error);
|
logger.error('Failed to find TMDB ID by IMDB ID:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -334,7 +351,7 @@ export class TMDBService {
|
||||||
await Promise.all(seasonPromises);
|
await Promise.all(seasonPromises);
|
||||||
return allEpisodes;
|
return allEpisodes;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get all episodes:', error);
|
logger.error('Failed to get all episodes:', error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -395,7 +412,7 @@ export class TMDBService {
|
||||||
crew: response.data.crew || []
|
crew: response.data.crew || []
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch credits:', error);
|
logger.error('Failed to fetch credits:', error);
|
||||||
return { cast: [], crew: [] };
|
return { cast: [], crew: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -410,7 +427,7 @@ export class TMDBService {
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch person details:', error);
|
logger.error('Failed to fetch person details:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -428,14 +445,14 @@ export class TMDBService {
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get show external IDs:', error);
|
logger.error('Failed to get show external IDs:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecommendations(type: 'movie' | 'tv', tmdbId: string): Promise<any[]> {
|
async getRecommendations(type: 'movie' | 'tv', tmdbId: string): Promise<any[]> {
|
||||||
if (!API_KEY) {
|
if (!API_KEY) {
|
||||||
console.error('TMDB API key not set');
|
logger.error('TMDB API key not set');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -445,7 +462,7 @@ export class TMDBService {
|
||||||
});
|
});
|
||||||
return response.data.results || [];
|
return response.data.results || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching TMDB ${type} recommendations for ID ${tmdbId}:`, error);
|
logger.error(`Error fetching TMDB ${type} recommendations for ID ${tmdbId}:`, error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -463,7 +480,7 @@ export class TMDBService {
|
||||||
});
|
});
|
||||||
return response.data.results;
|
return response.data.results;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to search multi:', error);
|
logger.error('Failed to search multi:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -476,7 +493,7 @@ export class TMDBService {
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching movie details:', error);
|
logger.error('Error fetching movie details:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -507,10 +524,53 @@ export class TMDBService {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching certification:', error);
|
logger.error('Error fetching certification:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get trending movies or TV shows
|
||||||
|
* @param type 'movie' or 'tv'
|
||||||
|
* @param timeWindow 'day' or 'week'
|
||||||
|
*/
|
||||||
|
async getTrending(type: 'movie' | 'tv', timeWindow: 'day' | 'week'): Promise<TMDBTrendingResult[]> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${BASE_URL}/trending/${type}/${timeWindow}`, {
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
params: {
|
||||||
|
language: 'en-US',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get external IDs for each trending item
|
||||||
|
const results = response.data.results || [];
|
||||||
|
const resultsWithExternalIds = await Promise.all(
|
||||||
|
results.map(async (item: TMDBTrendingResult) => {
|
||||||
|
try {
|
||||||
|
const externalIdsResponse = await axios.get(
|
||||||
|
`${BASE_URL}/${type}/${item.id}/external_ids`,
|
||||||
|
{
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
external_ids: externalIdsResponse.data
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to get external IDs for ${type} ${item.id}:`, error);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return resultsWithExternalIds;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to get trending ${type} content:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tmdbService = TMDBService.getInstance();
|
export const tmdbService = TMDBService.getInstance();
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
import { NativeModules, NativeEventEmitter, EmitterSubscription, Platform } from 'react-native';
|
import { NativeModules, NativeEventEmitter, EmitterSubscription, Platform } from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
// Mock implementation for Expo environment
|
// Mock implementation for Expo environment
|
||||||
const MockTorrentStreamModule = {
|
const MockTorrentStreamModule = {
|
||||||
TORRENT_PROGRESS_EVENT: 'torrentProgress',
|
TORRENT_PROGRESS_EVENT: 'torrentProgress',
|
||||||
startStream: async (magnetUri: string): Promise<string> => {
|
startStream: async (magnetUri: string): Promise<string> => {
|
||||||
console.log('[MockTorrentService] Starting mock stream for:', magnetUri);
|
logger.log('[MockTorrentService] Starting mock stream for:', magnetUri);
|
||||||
// Return a fake URL that would look like a file path
|
// Return a fake URL that would look like a file path
|
||||||
return `https://mock-torrent-stream.com/${magnetUri.substring(0, 10)}.mp4`;
|
return `https://mock-torrent-stream.com/${magnetUri.substring(0, 10)}.mp4`;
|
||||||
},
|
},
|
||||||
stopStream: () => {
|
stopStream: () => {
|
||||||
console.log('[MockTorrentService] Stopping mock stream');
|
logger.log('[MockTorrentService] Stopping mock stream');
|
||||||
},
|
},
|
||||||
fileExists: async (path: string): Promise<boolean> => {
|
fileExists: async (path: string): Promise<boolean> => {
|
||||||
console.log('[MockTorrentService] Checking if file exists:', path);
|
logger.log('[MockTorrentService] Checking if file exists:', path);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
// Add these methods to satisfy NativeModule interface
|
// Add these methods to satisfy NativeModule interface
|
||||||
|
|
@ -93,11 +94,11 @@ class TorrentService {
|
||||||
if (cacheData) {
|
if (cacheData) {
|
||||||
const cacheMap = JSON.parse(cacheData);
|
const cacheMap = JSON.parse(cacheData);
|
||||||
this.cachedTorrents = new Map(Object.entries(cacheMap));
|
this.cachedTorrents = new Map(Object.entries(cacheMap));
|
||||||
console.log('[TorrentService] Loaded cache mapping:', this.cachedTorrents);
|
logger.log('[TorrentService] Loaded cache mapping:', this.cachedTorrents);
|
||||||
}
|
}
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[TorrentService] Error loading cache:', error);
|
logger.error('[TorrentService] Error loading cache:', error);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,9 +107,9 @@ class TorrentService {
|
||||||
try {
|
try {
|
||||||
const cacheData = Object.fromEntries(this.cachedTorrents);
|
const cacheData = Object.fromEntries(this.cachedTorrents);
|
||||||
await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
|
await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
|
||||||
console.log('[TorrentService] Saved cache mapping');
|
logger.log('[TorrentService] Saved cache mapping');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[TorrentService] Error saving cache:', error);
|
logger.error('[TorrentService] Error saving cache:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,7 +123,7 @@ class TorrentService {
|
||||||
// First check if we have this torrent cached
|
// First check if we have this torrent cached
|
||||||
const cachedPath = this.cachedTorrents.get(magnetUri);
|
const cachedPath = this.cachedTorrents.get(magnetUri);
|
||||||
if (cachedPath) {
|
if (cachedPath) {
|
||||||
console.log('[TorrentService] Found cached torrent file:', cachedPath);
|
logger.log('[TorrentService] Found cached torrent file:', cachedPath);
|
||||||
|
|
||||||
// In mock mode, we'll always use the cached path if available
|
// In mock mode, we'll always use the cached path if available
|
||||||
if (!TorrentStreamModule) {
|
if (!TorrentStreamModule) {
|
||||||
|
|
@ -141,7 +142,7 @@ class TorrentService {
|
||||||
try {
|
try {
|
||||||
const exists = await TorrentStreamModule.fileExists(cachedPath);
|
const exists = await TorrentStreamModule.fileExists(cachedPath);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
console.log('[TorrentService] Using cached torrent file');
|
logger.log('[TorrentService] Using cached torrent file');
|
||||||
|
|
||||||
// Setup progress listener if callback provided
|
// Setup progress listener if callback provided
|
||||||
this.setupProgressListener(events);
|
this.setupProgressListener(events);
|
||||||
|
|
@ -150,12 +151,12 @@ class TorrentService {
|
||||||
await TorrentStreamModule.startStream(magnetUri);
|
await TorrentStreamModule.startStream(magnetUri);
|
||||||
return cachedPath;
|
return cachedPath;
|
||||||
} else {
|
} else {
|
||||||
console.log('[TorrentService] Cached file not found, removing from cache');
|
logger.log('[TorrentService] Cached file not found, removing from cache');
|
||||||
this.cachedTorrents.delete(magnetUri);
|
this.cachedTorrents.delete(magnetUri);
|
||||||
await this.saveCache();
|
await this.saveCache();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[TorrentService] Error checking cached file:', error);
|
logger.error('[TorrentService] Error checking cached file:', error);
|
||||||
// Continue to download again if there's an error
|
// Continue to download again if there's an error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +169,7 @@ class TorrentService {
|
||||||
|
|
||||||
// If we're in mock mode (Expo), simulate progress
|
// If we're in mock mode (Expo), simulate progress
|
||||||
if (!TorrentStreamModule) {
|
if (!TorrentStreamModule) {
|
||||||
console.log('[TorrentService] Using mock implementation');
|
logger.log('[TorrentService] Using mock implementation');
|
||||||
const mockUrl = `https://mock-torrent-stream.com/${magnetUri.substring(0, 10)}.mp4`;
|
const mockUrl = `https://mock-torrent-stream.com/${magnetUri.substring(0, 10)}.mp4`;
|
||||||
|
|
||||||
// Save to cache
|
// Save to cache
|
||||||
|
|
@ -185,19 +186,19 @@ class TorrentService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the actual stream if native module is available
|
// Start the actual stream if native module is available
|
||||||
console.log('[TorrentService] Starting torrent stream');
|
logger.log('[TorrentService] Starting torrent stream');
|
||||||
const filePath = await TorrentStreamModule.startStream(magnetUri);
|
const filePath = await TorrentStreamModule.startStream(magnetUri);
|
||||||
|
|
||||||
// Save to cache
|
// Save to cache
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
console.log('[TorrentService] Adding path to cache:', filePath);
|
logger.log('[TorrentService] Adding path to cache:', filePath);
|
||||||
this.cachedTorrents.set(magnetUri, filePath);
|
this.cachedTorrents.set(magnetUri, filePath);
|
||||||
await this.saveCache();
|
await this.saveCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[TorrentService] Error starting torrent stream:', error);
|
logger.error('[TorrentService] Error starting torrent stream:', error);
|
||||||
this.cleanup(); // Clean up on error
|
this.cleanup(); // Clean up on error
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -205,18 +206,18 @@ class TorrentService {
|
||||||
|
|
||||||
private setupProgressListener(events?: TorrentStreamEvents) {
|
private setupProgressListener(events?: TorrentStreamEvents) {
|
||||||
if (events?.onProgress) {
|
if (events?.onProgress) {
|
||||||
console.log('[TorrentService] Setting up progress listener');
|
logger.log('[TorrentService] Setting up progress listener');
|
||||||
this.progressListener = this.eventEmitter.addListener(
|
this.progressListener = this.eventEmitter.addListener(
|
||||||
TorrentService.TORRENT_PROGRESS_EVENT,
|
TorrentService.TORRENT_PROGRESS_EVENT,
|
||||||
(progress) => {
|
(progress) => {
|
||||||
console.log('[TorrentService] Progress event received:', progress);
|
logger.log('[TorrentService] Progress event received:', progress);
|
||||||
if (events.onProgress) {
|
if (events.onProgress) {
|
||||||
events.onProgress(progress);
|
events.onProgress(progress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('[TorrentService] No progress callback provided');
|
logger.log('[TorrentService] No progress callback provided');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,7 +262,7 @@ class TorrentService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stopStreamAndWait(): Promise<void> {
|
public async stopStreamAndWait(): Promise<void> {
|
||||||
console.log('[TorrentService] Stopping stream and waiting for cleanup');
|
logger.log('[TorrentService] Stopping stream and waiting for cleanup');
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
|
||||||
if (TorrentStreamModule) {
|
if (TorrentStreamModule) {
|
||||||
|
|
@ -270,35 +271,35 @@ class TorrentService {
|
||||||
// Wait a moment to ensure native side has cleaned up
|
// Wait a moment to ensure native side has cleaned up
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[TorrentService] Error stopping torrent stream:', error);
|
logger.error('[TorrentService] Error stopping torrent stream:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopStream(): void {
|
public stopStream(): void {
|
||||||
try {
|
try {
|
||||||
console.log('[TorrentService] Stopping stream and cleaning up');
|
logger.log('[TorrentService] Stopping stream and cleaning up');
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
|
||||||
if (TorrentStreamModule) {
|
if (TorrentStreamModule) {
|
||||||
TorrentStreamModule.stopStream();
|
TorrentStreamModule.stopStream();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[TorrentService] Error stopping torrent stream:', error);
|
logger.error('[TorrentService] Error stopping torrent stream:', error);
|
||||||
// Still attempt cleanup even if stop fails
|
// Still attempt cleanup even if stop fails
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
console.log('[TorrentService] Cleaning up event listeners and intervals');
|
logger.log('[TorrentService] Cleaning up event listeners and intervals');
|
||||||
|
|
||||||
// Clean up progress listener
|
// Clean up progress listener
|
||||||
if (this.progressListener) {
|
if (this.progressListener) {
|
||||||
try {
|
try {
|
||||||
this.progressListener.remove();
|
this.progressListener.remove();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[TorrentService] Error removing progress listener:', error);
|
logger.error('[TorrentService] Error removing progress listener:', error);
|
||||||
} finally {
|
} finally {
|
||||||
this.progressListener = null;
|
this.progressListener = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
src/utils/logger.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
class Logger {
|
||||||
|
private isEnabled: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// __DEV__ is a global variable in React Native
|
||||||
|
this.isEnabled = __DEV__;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(...args: any[]) {
|
||||||
|
if (this.isEnabled) {
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error(...args: any[]) {
|
||||||
|
if (this.isEnabled) {
|
||||||
|
console.error(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(...args: any[]) {
|
||||||
|
if (this.isEnabled) {
|
||||||
|
console.warn(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info(...args: any[]) {
|
||||||
|
if (this.isEnabled) {
|
||||||
|
console.info(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(...args: any[]) {
|
||||||
|
if (this.isEnabled) {
|
||||||
|
console.debug(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = new Logger();
|
||||||