some changes

This commit is contained in:
Nayif Noushad 2025-04-13 11:20:56 +05:30
parent 6e62e4cb9b
commit 1575bdd86f
52 changed files with 754 additions and 11847 deletions

11
.expo-shared/README.md Normal file
View 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
View 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
}

View file

@ -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)
} }

View file

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

View file

@ -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)
} }

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -21,7 +21,7 @@ extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
} }
} }
rootProject.name = 'stremio-expo' rootProject.name = 'Nuvio'
dependencyResolutionManagement { dependencyResolutionManagement {
versionCatalogs { versionCatalogs {

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View file

@ -5,5 +5,10 @@ module.exports = function (api) {
plugins: [ plugins: [
'react-native-reanimated/plugin', 'react-native-reanimated/plugin',
], ],
env: {
production: {
plugins: ['transform-remove-console'],
},
},
}; };
}; };

View file

@ -19,6 +19,11 @@
"android": { "android": {
"buildType": "app-bundle" "buildType": "app-bundle"
} }
},
"apk": {
"android": {
"buildType": "apk"
}
} }
}, },
"submit": { "submit": {

View file

@ -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
View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

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

View file

@ -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) {

View file

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

View file

@ -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 && (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}, },
}); });

View file

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

View file

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

View file

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

View file

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

View file

@ -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})`);
}} }}
/> />

View file

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

View file

@ -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;
} }
} }

View file

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

View file

@ -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 {};
} }
} }

View file

@ -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}`);

View file

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

View file

@ -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
View 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();