some changes
11
.expo-shared/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
> Why do I have a folder named ".expo-shared" in my project?
|
||||
|
||||
The ".expo-shared" folder is created when running commands that produce state that is intended to be shared with all developers on the project. For example, "npx expo-optimize".
|
||||
|
||||
> What does the "assets.json" file contain?
|
||||
|
||||
The "assets.json" file describes the assets that have been optimized through "expo-optimize" and do not need to be processed again.
|
||||
|
||||
> Should I commit the ".expo-shared" folder?
|
||||
|
||||
Yes, you should share the ".expo-shared" folder with your collaborators.
|
||||
19
.expo-shared/assets.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"1377cee8d8f2cab2c4146574910c2b66b1a92365bd9dc945f9240c6727aee0be": true,
|
||||
"015a72aeb24c1166243ebc185355c9488cc892fbf3aa82f9ae66819a10f3451c": true,
|
||||
"7d8138ee6378ca981d3b887798bd6f3d101a24012f58b96ba616ff7f4b179cd2": true,
|
||||
"f14426796829173c18cb16bbb7839d370726835bcd16bf8a1813a0c418a4cf2e": true,
|
||||
"b60b46556bf2a07fb4f1e14feb1728f2690c6d8e337ecd7b78f1cdea7ccbe2bf": true,
|
||||
"107386eaf4e7f0d9c8256b3ff62011ec9dff059bb8df975bf8dae66729127606": true,
|
||||
"5f4c0a732b6325bf4071d9124d2ae67e037cb24fcc9c482ef82bea742109a3b8": true,
|
||||
"1115b80ca5da0e724a39b7f789771a42959d47aab327f36a2710fad69200857d": true,
|
||||
"f543ece2dea5881d69abf9eb661da82deafb4b6ea8e48a73b628db0a88750816": true,
|
||||
"30aa92b9e2028ff6ae19bcc91693d0d3d65d459c797ae882844472fa2f810cfa": true,
|
||||
"77294e294a44cc6a0c5ac6d55b50733697be46cfb09322d1d84866ebc3272bed": true,
|
||||
"24272cdaeff82cc5facdaccd982a6f05b60c4504704bbf94c19a6388659880bb": true,
|
||||
"8b8c429e5130b53fa2caf330b76dfbf66e25109acb76f5f3f34325f25bb70274": true,
|
||||
"7fc3cf4d858d741cb3078f63a8b3cb20951948e68c08309740f438f90b8480aa": true,
|
||||
"d5997fd2d5ea21b27c3d124f04f79709744936ee083972aa868ddb6db4621d6f": true,
|
||||
"81955b3156f2a65ec124956520ba74c6dd686d4a8a7cc06c1f6a415495d32314": true,
|
||||
"71633e3d0bb9fbdb4956fd33aafdd4bfdb4cddc3f5f820f72b41b406fb23afdb": true
|
||||
}
|
||||
|
|
@ -86,9 +86,19 @@ android {
|
|||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
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 {
|
||||
applicationId 'com.stremio.expo'
|
||||
applicationId 'com.nuvio.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
|
|
@ -110,8 +120,8 @@ android {
|
|||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<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.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<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>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
|
@ -27,7 +25,7 @@
|
|||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="com.stremio.expo"/>
|
||||
<data android:scheme="com.nuvio.app"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
package com.stremio.expo
|
||||
import expo.modules.splashscreen.SplashScreenManager
|
||||
package com.nuvio.app
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
|
@ -16,10 +15,7 @@ class MainActivity : ReactActivity() {
|
|||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
// setTheme(R.style.AppTheme);
|
||||
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
|
||||
SplashScreenManager.registerOnActivity(this)
|
||||
// @generated end expo-splashscreen
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null)
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.stremio.expo
|
||||
package com.nuvio.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<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_status_bar_translucent" translatable="false">false</string>
|
||||
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@
|
|||
<item name="android:textColorHint">#c8c8c8</item>
|
||||
<item name="android:textColor">@android:color/black</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -21,7 +21,7 @@ extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
|||
}
|
||||
}
|
||||
|
||||
rootProject.name = 'stremio-expo'
|
||||
rootProject.name = 'Nuvio'
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
|
|
|
|||
6
app.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"expo": {
|
||||
"name": "stremio-expo",
|
||||
"slug": "stremio-expo",
|
||||
"name": "Nuvio",
|
||||
"slug": "nuvio",
|
||||
"version": "1.0.0",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/icon.png",
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
"INTERNET",
|
||||
"WAKE_LOCK"
|
||||
],
|
||||
"package": "com.stremio.expo"
|
||||
"package": "com.nuvio.app"
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.6 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 720 B |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 56 KiB |
|
|
@ -5,5 +5,10 @@ module.exports = function (api) {
|
|||
plugins: [
|
||||
'react-native-reanimated/plugin',
|
||||
],
|
||||
env: {
|
||||
production: {
|
||||
plugins: ['transform-remove-console'],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
5
eas.json
|
|
@ -19,6 +19,11 @@
|
|||
"android": {
|
||||
"buildType": "app-bundle"
|
||||
}
|
||||
},
|
||||
"apk": {
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@ module.exports = (() => {
|
|||
config.transformer = {
|
||||
...transformer,
|
||||
babelTransformerPath: require.resolve('react-native-svg-transformer'),
|
||||
minifierConfig: {
|
||||
compress: {
|
||||
// Remove console.* statements in release builds
|
||||
drop_console: true,
|
||||
// Keep error logging for critical issues
|
||||
pure_funcs: ['console.info', 'console.log', 'console.debug', 'console.warn'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
config.resolver = {
|
||||
|
|
|
|||
84
package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "stremio-expo",
|
||||
"name": "nuvio",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "stremio-expo",
|
||||
"name": "nuvio",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.1.0",
|
||||
|
|
@ -21,21 +21,16 @@
|
|||
"axios": "^1.8.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"expo": "~52.0.43",
|
||||
"expo-av": "^15.0.2",
|
||||
"expo-blur": "^14.0.3",
|
||||
"expo-haptics": "~14.0.1",
|
||||
"expo-image": "~2.0.7",
|
||||
"expo-linear-gradient": "~14.0.2",
|
||||
"expo-notifications": "~0.29.14",
|
||||
"expo-screen-orientation": "~8.0.4",
|
||||
"expo-sharing": "^13.0.1",
|
||||
"expo-splash-screen": "^0.29.22",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-system-ui": "^4.0.9",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-awesome-slider": "^2.9.0",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-immersive-mode": "^2.0.2",
|
||||
"react-native-paper": "^5.13.1",
|
||||
|
|
@ -49,6 +44,7 @@
|
|||
"@babel/core": "^7.25.2",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-native": "^0.72.8",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"react-native-svg-transformer": "^1.5.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
|
|
@ -4920,6 +4916,13 @@
|
|||
"@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": {
|
||||
"version": "1.1.0",
|
||||
"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": "*"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.8.tgz",
|
||||
|
|
@ -6757,27 +6732,6 @@
|
|||
"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": {
|
||||
"version": "2.0.1",
|
||||
"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": {
|
||||
"version": "2.20.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "stremio-expo",
|
||||
"name": "nuvio",
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
|
|
@ -22,21 +22,16 @@
|
|||
"axios": "^1.8.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"expo": "~52.0.43",
|
||||
"expo-av": "^15.0.2",
|
||||
"expo-blur": "^14.0.3",
|
||||
"expo-haptics": "~14.0.1",
|
||||
"expo-image": "~2.0.7",
|
||||
"expo-linear-gradient": "~14.0.2",
|
||||
"expo-notifications": "~0.29.14",
|
||||
"expo-screen-orientation": "~8.0.4",
|
||||
"expo-sharing": "^13.0.1",
|
||||
"expo-splash-screen": "^0.29.22",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-system-ui": "^4.0.9",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-awesome-slider": "^2.9.0",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-immersive-mode": "^2.0.2",
|
||||
"react-native-paper": "^5.13.1",
|
||||
|
|
@ -50,6 +45,7 @@
|
|||
"@babel/core": "^7.25.2",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-native": "^0.72.8",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"react-native-svg-transformer": "^1.5.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 11 KiB |
|
|
@ -1,26 +1,48 @@
|
|||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
import { StreamingContent } from '../services/catalogService';
|
||||
|
||||
interface CatalogContextType {
|
||||
lastUpdate: number;
|
||||
refreshCatalogs: () => void;
|
||||
addToLibrary: (content: StreamingContent) => void;
|
||||
removeFromLibrary: (type: string, id: string) => void;
|
||||
libraryItems: StreamingContent[];
|
||||
}
|
||||
|
||||
const CatalogContext = createContext<CatalogContextType>({
|
||||
lastUpdate: Date.now(),
|
||||
refreshCatalogs: () => {},
|
||||
addToLibrary: () => {},
|
||||
removeFromLibrary: () => {},
|
||||
libraryItems: []
|
||||
});
|
||||
|
||||
export const useCatalogContext = () => useContext(CatalogContext);
|
||||
|
||||
export const CatalogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [lastUpdate, setLastUpdate] = useState(Date.now());
|
||||
const [libraryItems, setLibraryItems] = useState<StreamingContent[]>([]);
|
||||
|
||||
const refreshCatalogs = useCallback(() => {
|
||||
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 (
|
||||
<CatalogContext.Provider value={{ lastUpdate, refreshCatalogs }}>
|
||||
<CatalogContext.Provider value={{
|
||||
lastUpdate,
|
||||
refreshCatalogs,
|
||||
addToLibrary,
|
||||
removeFromLibrary,
|
||||
libraryItems
|
||||
}}>
|
||||
{children}
|
||||
</CatalogContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { tmdbService } from '../services/tmdbService';
|
|||
import { cacheService } from '../services/cacheService';
|
||||
import { Cast, Episode, GroupedEpisodes, GroupedStreams } from '../types/metadata';
|
||||
import { TMDBService } from '../services/tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Constants for timeouts and retries
|
||||
const API_TIMEOUT = 10000; // 10 seconds
|
||||
|
|
@ -31,7 +32,7 @@ const loadWithFallback = async <T>(
|
|||
try {
|
||||
return await withTimeout(loadFn(), timeout, fallback);
|
||||
} catch (error) {
|
||||
console.error('Loading failed, using fallback:', error);
|
||||
logger.error('Loading failed, using fallback:', error);
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
|
@ -115,9 +116,9 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams';
|
||||
|
||||
try {
|
||||
console.log(`🔍 [${logPrefix}:${sourceType}] Starting fetch`);
|
||||
logger.log(`🔍 [${logPrefix}:${sourceType}] Starting fetch`);
|
||||
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 (Object.keys(result).length > 0) {
|
||||
|
|
@ -126,7 +127,7 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
return acc + (group.streams?.length || 0);
|
||||
}, 0);
|
||||
|
||||
console.log(`📦 [${logPrefix}:${sourceType}] Found ${totalStreams} streams`);
|
||||
logger.log(`📦 [${logPrefix}:${sourceType}] Found ${totalStreams} streams`);
|
||||
|
||||
// Update state for this source
|
||||
if (isEpisode) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { LinearGradient } from 'expo-linear-gradient';
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Extend Manifest type to include logo
|
||||
interface ExtendedManifest extends Manifest {
|
||||
|
|
@ -58,7 +59,7 @@ const AddonsScreen = () => {
|
|||
const installedAddons = await stremioService.getInstalledAddonsAsync();
|
||||
setAddons(installedAddons);
|
||||
} catch (error) {
|
||||
console.error('Failed to load addons:', error);
|
||||
logger.error('Failed to load addons:', error);
|
||||
Alert.alert('Error', 'Failed to load addons');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -79,7 +80,7 @@ const AddonsScreen = () => {
|
|||
setShowAddModal(false);
|
||||
setShowConfirmModal(true);
|
||||
} 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');
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
|
|
@ -98,7 +99,7 @@ const AddonsScreen = () => {
|
|||
loadAddons();
|
||||
Alert.alert('Success', 'Addon installed successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to install addon:', error);
|
||||
logger.error('Failed to install addon:', error);
|
||||
Alert.alert('Error', 'Failed to install addon');
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { format, parseISO, isThisWeek, isAfter, startOfToday, addWeeks, isBefore
|
|||
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||
import { CalendarSection } from '../components/calendar/CalendarSection';
|
||||
import { tmdbService } from '../services/tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ interface CalendarSection {
|
|||
const CalendarScreen = () => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
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 [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
|
@ -60,13 +61,13 @@ const CalendarScreen = () => {
|
|||
const [filteredEpisodes, setFilteredEpisodes] = useState<CalendarEpisode[]>([]);
|
||||
|
||||
const fetchCalendarData = useCallback(async () => {
|
||||
console.log("[Calendar] Starting to fetch calendar data");
|
||||
logger.log("[Calendar] Starting to fetch calendar data");
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Filter for only series in library
|
||||
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 seriesWithoutEpisodes: CalendarEpisode[] = [];
|
||||
|
|
@ -74,12 +75,12 @@ const CalendarScreen = () => {
|
|||
// For each series, fetch upcoming episodes
|
||||
for (const series of seriesItems) {
|
||||
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);
|
||||
console.log(`[Calendar] Metadata fetched:`, metadata ? 'success' : 'null');
|
||||
logger.log(`[Calendar] Metadata fetched:`, metadata ? 'success' : 'null');
|
||||
|
||||
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
|
||||
const today = startOfToday();
|
||||
const fourWeeksLater = addWeeks(today, 4);
|
||||
|
|
@ -161,7 +162,7 @@ const CalendarScreen = () => {
|
|||
});
|
||||
}
|
||||
} 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))
|
||||
);
|
||||
|
||||
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[] = [];
|
||||
|
||||
|
|
@ -207,7 +208,7 @@ const CalendarScreen = () => {
|
|||
|
||||
setCalendarData(sections);
|
||||
} catch (error) {
|
||||
console.error('Error fetching calendar data:', error);
|
||||
logger.error('Error fetching calendar data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
|
|
@ -216,10 +217,10 @@ const CalendarScreen = () => {
|
|||
|
||||
useEffect(() => {
|
||||
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();
|
||||
} 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);
|
||||
}
|
||||
}, [libraryItems, libraryLoading, fetchCalendarData]);
|
||||
|
|
@ -358,11 +359,11 @@ const CalendarScreen = () => {
|
|||
[...acc, ...section.data], [] as CalendarEpisode[]);
|
||||
|
||||
// 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
|
||||
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);
|
||||
|
||||
// Filter episodes for the selected date
|
||||
|
|
@ -372,13 +373,13 @@ const CalendarScreen = () => {
|
|||
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);
|
||||
}, [allEpisodes]);
|
||||
|
||||
// Reset date filter
|
||||
const clearDateFilter = useCallback(() => {
|
||||
console.log(`[Calendar] Clearing date filter`);
|
||||
logger.log(`[Calendar] Clearing date filter`);
|
||||
setSelectedDate(null);
|
||||
setFilteredEpisodes([]);
|
||||
}, []);
|
||||
|
|
@ -444,7 +445,7 @@ const CalendarScreen = () => {
|
|||
<MaterialIcons name="arrow-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>Calendar</Text>
|
||||
<View style={{ width: 40 }} /> {/* Empty view for balance */}
|
||||
<View style={{ width: 40 }} />
|
||||
</View>
|
||||
|
||||
{selectedDate && filteredEpisodes.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
|||
import { Meta, stremioService } from '../services/stremioService';
|
||||
import { colors } from '../styles';
|
||||
import { Image } from 'expo-image';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
type CatalogScreenProps = {
|
||||
route: RouteProp<RootStackParamList, 'Catalog'>;
|
||||
|
|
@ -104,12 +105,12 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
foundItems = true;
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Failed to process addon ${manifest.name}:`, error);
|
||||
logger.log(`Failed to process addon ${manifest.name}:`, error);
|
||||
// Continue with other addons
|
||||
}
|
||||
}
|
||||
|
|
@ -140,7 +141,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
}
|
||||
} catch (err) {
|
||||
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 {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { colors } from '../styles';
|
|||
import { stremioService } from '../services/stremioService';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
interface CatalogSetting {
|
||||
addonId: string;
|
||||
|
|
@ -112,7 +113,7 @@ const CatalogSettingsScreen = () => {
|
|||
|
||||
setSettings(sortedCatalogs);
|
||||
} catch (error) {
|
||||
console.error('Failed to load catalog settings:', error);
|
||||
logger.error('Failed to load catalog settings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -131,7 +132,7 @@ const CatalogSettingsScreen = () => {
|
|||
await AsyncStorage.setItem(CATALOG_SETTINGS_KEY, JSON.stringify(settingsObj));
|
||||
refreshCatalogs(); // Trigger catalog refresh after saving settings
|
||||
} catch (error) {
|
||||
console.error('Failed to save catalog settings:', error);
|
||||
logger.error('Failed to save catalog settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { Image } from 'expo-image';
|
|||
import Animated, { FadeIn, FadeOut, SlideInRight, Layout } from 'react-native-reanimated';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
interface Category {
|
||||
id: string;
|
||||
|
|
@ -307,7 +308,7 @@ const DiscoverScreen = () => {
|
|||
setCatalogs([{ genre, items: content }]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load content:', error);
|
||||
logger.error('Failed to load content:', error);
|
||||
setCatalogs([]);
|
||||
setAllContent([]);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ import {
|
|||
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||
import { ThisWeekSection } from '../components/home/ThisWeekSection';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { tmdbService } from '../services/tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Define interfaces for our data
|
||||
interface Category {
|
||||
|
|
@ -213,6 +215,7 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
|||
const [localItem, setLocalItem] = useState(initialItem);
|
||||
const [isWatched, setIsWatched] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
setMenuVisible(true);
|
||||
|
|
@ -276,11 +279,24 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
|||
contentFit="cover"
|
||||
transition={200}
|
||||
cachePolicy="memory-disk"
|
||||
recyclingKey={`poster-${localItem.id}`}
|
||||
onLoadStart={() => {
|
||||
setImageLoaded(false);
|
||||
setImageError(false);
|
||||
}}
|
||||
onLoadEnd={() => setImageLoaded(true)}
|
||||
onError={() => {
|
||||
setImageError(true);
|
||||
setImageLoaded(true);
|
||||
}}
|
||||
/>
|
||||
{!imageLoaded && (
|
||||
{(!imageLoaded || imageError) && (
|
||||
<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>
|
||||
)}
|
||||
{isWatched && (
|
||||
|
|
@ -352,6 +368,29 @@ const SkeletonFeatured = () => (
|
|||
</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 navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
|
|
@ -366,6 +405,39 @@ const HomeScreen = () => {
|
|||
const maxRetries = 3;
|
||||
const { lastUpdate } = useCatalogContext();
|
||||
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(() => {
|
||||
StatusBar.setTranslucent(true);
|
||||
|
|
@ -386,23 +458,9 @@ const HomeScreen = () => {
|
|||
};
|
||||
}, [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[]) => {
|
||||
if (!content.length) return;
|
||||
|
||||
try {
|
||||
setLoadingImages(true);
|
||||
const imagePromises = content.map(item => {
|
||||
|
|
@ -428,108 +486,112 @@ const HomeScreen = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const loadContent = useCallback(async (forceRefresh = false) => {
|
||||
const loadFeaturedContent = useCallback(async () => {
|
||||
try {
|
||||
if (!forceRefresh && !loading && !refreshing) {
|
||||
setLoading(true);
|
||||
}
|
||||
const trendingResults = await tmdbService.getTrending('movie', 'day');
|
||||
|
||||
// Helper function to delay execution
|
||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// Try loading content with retries
|
||||
let attempt = 0;
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
// Clear existing catalogs when forcing refresh
|
||||
if (forceRefresh) {
|
||||
setCatalogs([]);
|
||||
setAllFeaturedContent([]);
|
||||
setFeaturedContent(null);
|
||||
}
|
||||
|
||||
// Load catalogs from service
|
||||
const homeCatalogs = await catalogService.getHomeCatalogs();
|
||||
|
||||
// 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);
|
||||
}
|
||||
if (trendingResults.length > 0) {
|
||||
const formattedContent: StreamingContent[] = trendingResults
|
||||
.filter(item => item.title || item.name) // Filter out items without a name
|
||||
.map(item => {
|
||||
const yearString = (item.release_date || item.first_air_date)?.substring(0, 4);
|
||||
return {
|
||||
id: `tmdb:${item.id}`,
|
||||
type: 'movie',
|
||||
name: item.title || item.name || 'Unknown Title',
|
||||
poster: tmdbService.getImageUrl(item.poster_path) || '',
|
||||
banner: tmdbService.getImageUrl(item.backdrop_path) || '',
|
||||
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,
|
||||
genres: item.genre_ids.map(id => GENRE_MAP[id] || id.toString()),
|
||||
inLibrary: false,
|
||||
};
|
||||
});
|
||||
|
||||
const uniqueCatalogs = Array.from(uniqueCatalogsMap.values());
|
||||
const popularCatalog = uniqueCatalogs.find(catalog =>
|
||||
catalog.name.toLowerCase().includes('popular') ||
|
||||
catalog.name.toLowerCase().includes('top') ||
|
||||
catalog.id.toLowerCase().includes('top')
|
||||
);
|
||||
setAllFeaturedContent(formattedContent);
|
||||
// Randomly select a featured item
|
||||
const randomIndex = Math.floor(Math.random() * formattedContent.length);
|
||||
setFeaturedContent(formattedContent[randomIndex]);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to load featured content:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Set catalogs showing all unique content
|
||||
setCatalogs(uniqueCatalogs);
|
||||
const loadCatalogs = useCallback(async () => {
|
||||
// Create new abort controller for this load operation
|
||||
cleanup();
|
||||
abortControllerRef.current = new AbortController();
|
||||
const signal = abortControllerRef.current.signal;
|
||||
|
||||
// Set featured content and preload images
|
||||
if (popularCatalog && popularCatalog.items.length > 0) {
|
||||
setAllFeaturedContent(popularCatalog.items);
|
||||
const randomIndex = Math.floor(Math.random() * popularCatalog.items.length);
|
||||
setFeaturedContent(popularCatalog.items[randomIndex]);
|
||||
|
||||
// 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));
|
||||
}
|
||||
try {
|
||||
// Load catalogs from service
|
||||
const homeCatalogs = await catalogService.getHomeCatalogs();
|
||||
|
||||
if (signal.aborted) return;
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
attempt++;
|
||||
console.error(`Failed to load content (attempt ${attempt}):`, error);
|
||||
if (attempt < maxRetries) {
|
||||
await delay(2000);
|
||||
}
|
||||
}
|
||||
// If no catalogs found, wait and retry
|
||||
if (!homeCatalogs?.length) {
|
||||
console.log('No catalogs found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('All attempts to load content failed');
|
||||
setCatalogs([]);
|
||||
setAllFeaturedContent([]);
|
||||
setFeaturedContent(null);
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
if (signal.aborted) return;
|
||||
|
||||
const uniqueCatalogs = Array.from(uniqueCatalogsMap.values());
|
||||
setCatalogs(uniqueCatalogs);
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Error in loadCatalogs:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
if (!signal.aborted) {
|
||||
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(() => {
|
||||
setRefreshing(true);
|
||||
loadContent(true);
|
||||
}, [loadContent]);
|
||||
|
||||
// Load content initially and when lastUpdate changes
|
||||
useEffect(() => {
|
||||
loadContent(true);
|
||||
}, [loadContent, lastUpdate]);
|
||||
Promise.all([
|
||||
loadFeaturedContent(),
|
||||
loadCatalogs(),
|
||||
]).catch(error => {
|
||||
logger.error('Error during refresh:', error);
|
||||
}).finally(() => {
|
||||
setRefreshing(false);
|
||||
});
|
||||
}, [loadFeaturedContent, loadCatalogs]);
|
||||
|
||||
// Check if content is in library
|
||||
useEffect(() => {
|
||||
|
|
@ -668,10 +730,19 @@ const HomeScreen = () => {
|
|||
|
||||
<TouchableOpacity
|
||||
style={styles.infoButton}
|
||||
onPress={() => navigation.navigate('Metadata', {
|
||||
id: featuredContent?.id,
|
||||
type: featuredContent?.type
|
||||
})}
|
||||
onPress={async () => {
|
||||
if (featuredContent) {
|
||||
// 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} />
|
||||
<Text style={styles.infoButtonText}>Info</Text>
|
||||
|
|
@ -719,7 +790,7 @@ const HomeScreen = () => {
|
|||
<FlatList
|
||||
data={item.items}
|
||||
renderItem={renderContentItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
keyExtractor={(item) => `${item.id}-${item.type}`}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.catalogList}
|
||||
|
|
@ -727,6 +798,15 @@ const HomeScreen = () => {
|
|||
decelerationRate="fast"
|
||||
snapToAlignment="start"
|
||||
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>
|
||||
);
|
||||
|
|
@ -780,6 +860,10 @@ const HomeScreen = () => {
|
|||
renderItem={renderCatalog}
|
||||
keyExtractor={(item, index) => `${item.addon}-${item.id}-${index}`}
|
||||
scrollEnabled={false}
|
||||
removeClippedSubviews={false}
|
||||
initialNumToRender={3}
|
||||
maxToRenderPerBatch={3}
|
||||
windowSize={5}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.emptyCatalog}>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
|||
import { catalogService } from '../services/catalogService';
|
||||
import type { StreamingContent } from '../services/catalogService';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Types
|
||||
interface LibraryItem extends StreamingContent {
|
||||
|
|
@ -103,7 +104,7 @@ const LibraryScreen = () => {
|
|||
const items = await catalogService.getLibraryItems();
|
||||
setLibraryItems(items);
|
||||
} catch (error) {
|
||||
console.error('Failed to load library:', error);
|
||||
logger.error('Failed to load library:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import { NavigationProp } from '@react-navigation/native';
|
|||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { TMDBService } from '../services/tmdbService';
|
||||
import { storageService } from '../services/storageService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ const springConfig = {
|
|||
};
|
||||
|
||||
// Add debug log for storageService
|
||||
console.log('[MetadataScreen] StorageService instance:', storageService);
|
||||
logger.log('[MetadataScreen] StorageService instance:', storageService);
|
||||
|
||||
const MetadataScreen = () => {
|
||||
const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string }>, string>>();
|
||||
|
|
@ -101,8 +102,15 @@ const MetadataScreen = () => {
|
|||
episodeId?: string;
|
||||
} | 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
|
||||
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
|
||||
const getEpisodeDetails = useCallback((episodeId: string) => {
|
||||
|
|
@ -260,7 +268,7 @@ const MetadataScreen = () => {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MetadataScreen] Error loading watch progress:', error);
|
||||
logger.error('[MetadataScreen] Error loading watch progress:', error);
|
||||
setWatchProgress(null);
|
||||
}
|
||||
}, [id, type, episodeId, episodes]);
|
||||
|
|
@ -292,9 +300,70 @@ const MetadataScreen = () => {
|
|||
return 'Resume';
|
||||
}, [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 = () => {
|
||||
if (!watchProgress) {
|
||||
if (!watchProgress || watchProgress.duration === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -310,7 +379,7 @@ const MetadataScreen = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={styles.watchProgressContainer}>
|
||||
<Animated.View style={[styles.watchProgressContainer, watchProgressAnimatedStyle]}>
|
||||
<View style={styles.watchProgressBar}>
|
||||
<View
|
||||
style={[
|
||||
|
|
@ -322,7 +391,7 @@ const MetadataScreen = () => {
|
|||
<Text style={styles.watchProgressText}>
|
||||
{progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`}{episodeInfo} • Last watched on {formattedTime}
|
||||
</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -366,7 +435,7 @@ const MetadataScreen = () => {
|
|||
if (tmdbId) {
|
||||
navigation.navigate('ShowRatings', { showId: tmdbId });
|
||||
} 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]);
|
||||
|
||||
const handleSelectCastMember = (castMember: any) => {
|
||||
// TODO: Implement cast member selection
|
||||
console.log('Cast member selected:', castMember);
|
||||
logger.log('Cast member selected:', castMember);
|
||||
};
|
||||
|
||||
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
|
||||
React.useEffect(() => {
|
||||
if (metadata && metadata.id) {
|
||||
|
|
@ -493,7 +601,7 @@ const MetadataScreen = () => {
|
|||
|
||||
if (tmdbId) {
|
||||
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
|
||||
if (type === 'movie' && credits.crew) {
|
||||
|
|
@ -507,7 +615,7 @@ const MetadataScreen = () => {
|
|||
...metadata,
|
||||
directors
|
||||
});
|
||||
console.log("Updated directors:", directors);
|
||||
logger.log("Updated directors:", directors);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -528,12 +636,12 @@ const MetadataScreen = () => {
|
|||
...metadata,
|
||||
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) {
|
||||
console.error('Error fetching crew data:', error);
|
||||
logger.error('Error fetching crew data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -731,22 +839,26 @@ const MetadataScreen = () => {
|
|||
</View>
|
||||
|
||||
{/* Creator/Director Info */}
|
||||
{((metadata.directors && metadata.directors.length > 0) || (metadata.creators && metadata.creators.length > 0)) && (
|
||||
<View style={styles.creatorContainer}>
|
||||
{metadata.directors && metadata.directors.length > 0 && (
|
||||
<View style={styles.creatorSection}>
|
||||
<Text style={styles.creatorLabel}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text>
|
||||
<Text style={styles.creatorText}>{metadata.directors.join(', ')}</Text>
|
||||
</View>
|
||||
)}
|
||||
{metadata.creators && metadata.creators.length > 0 && (
|
||||
<View style={styles.creatorSection}>
|
||||
<Text style={styles.creatorLabel}>Creator{metadata.creators.length > 1 ? 's' : ''}:</Text>
|
||||
<Text style={styles.creatorText}>{metadata.creators.join(', ')}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.creatorContainer,
|
||||
creatorAnimatedStyle,
|
||||
{ minHeight: (metadata?.directors?.length || metadata?.creators?.length) ? 'auto' : 0 }
|
||||
]}
|
||||
>
|
||||
{metadata.directors && metadata.directors.length > 0 && (
|
||||
<View style={styles.creatorSection}>
|
||||
<Text style={styles.creatorLabel}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text>
|
||||
<Text style={styles.creatorText}>{metadata.directors.join(', ')}</Text>
|
||||
</View>
|
||||
)}
|
||||
{metadata.creators && metadata.creators.length > 0 && (
|
||||
<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 */}
|
||||
{metadata.description && (
|
||||
|
|
@ -1108,36 +1220,41 @@ const styles = StyleSheet.create({
|
|||
creatorContainer: {
|
||||
marginBottom: 2,
|
||||
paddingHorizontal: 16,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
creatorSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
marginBottom: 4,
|
||||
height: 20
|
||||
},
|
||||
creatorLabel: {
|
||||
color: colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginRight: 8,
|
||||
lineHeight: 20
|
||||
},
|
||||
creatorText: {
|
||||
color: colors.lightGray,
|
||||
fontSize: 14,
|
||||
flex: 1,
|
||||
lineHeight: 20
|
||||
},
|
||||
watchProgressContainer: {
|
||||
marginTop: 8,
|
||||
marginBottom: 16,
|
||||
marginBottom: 12,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
watchProgressBar: {
|
||||
width: '80%',
|
||||
width: '75%',
|
||||
height: 3,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
borderRadius: 1.5,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 8,
|
||||
marginBottom: 6
|
||||
},
|
||||
watchProgressFill: {
|
||||
height: '100%',
|
||||
|
|
@ -1148,6 +1265,8 @@ const styles = StyleSheet.create({
|
|||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
opacity: 0.9,
|
||||
letterSpacing: 0.2
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { colors } from '../styles/colors';
|
|||
import { notificationService, NotificationSettings } from '../services/notificationService';
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const NotificationSettingsScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
|
|
@ -36,7 +37,7 @@ const NotificationSettingsScreen = () => {
|
|||
const savedSettings = await notificationService.getSettings();
|
||||
setSettings(savedSettings);
|
||||
} catch (error) {
|
||||
console.error('Error loading notification settings:', error);
|
||||
logger.error('Error loading notification settings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -84,7 +85,7 @@ const NotificationSettingsScreen = () => {
|
|||
// Update local state
|
||||
setSettings(updatedSettings);
|
||||
} catch (error) {
|
||||
console.error('Error updating notification settings:', error);
|
||||
logger.error('Error updating notification settings:', error);
|
||||
Alert.alert('Error', 'Failed to update notification settings');
|
||||
}
|
||||
};
|
||||
|
|
@ -111,7 +112,7 @@ const NotificationSettingsScreen = () => {
|
|||
await notificationService.cancelAllNotifications();
|
||||
Alert.alert('Success', 'All notifications have been reset');
|
||||
} catch (error) {
|
||||
console.error('Error resetting notifications:', error);
|
||||
logger.error('Error resetting notifications:', error);
|
||||
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.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error scheduling test notification:', error);
|
||||
logger.error('Error scheduling test notification:', error);
|
||||
Alert.alert('Error', 'Failed to schedule test notification');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import debounce from 'lodash/debounce';
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import Animated, { FadeIn, FadeOut, SlideInRight } from 'react-native-reanimated';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const HORIZONTAL_ITEM_WIDTH = width * 0.3;
|
||||
|
|
@ -117,7 +118,7 @@ const SearchScreen = () => {
|
|||
setRecentSearches(JSON.parse(savedSearches));
|
||||
}
|
||||
} 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);
|
||||
await AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
|
||||
} 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);
|
||||
await saveRecentSearch(searchQuery);
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
logger.error('Search failed:', error);
|
||||
setResults([]);
|
||||
} finally {
|
||||
setSearching(false);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { RouteProp } from '@react-navigation/native';
|
|||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import axios from 'axios';
|
||||
import Animated, { FadeIn, SlideInRight, withTiming, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
type RootStackParamList = {
|
||||
ShowRatings: { showId: number };
|
||||
|
|
@ -228,7 +229,7 @@ const ShowRatingsScreen = ({ route }: Props) => {
|
|||
}
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading more seasons:', error);
|
||||
logger.error('Error loading more seasons:', error);
|
||||
} finally {
|
||||
setLoadingProgress(0);
|
||||
setLoadingSeasons(false);
|
||||
|
|
@ -314,7 +315,7 @@ const ShowRatingsScreen = ({ route }: Props) => {
|
|||
setVisibleSeasonRange({ start: 0, end: initialEnd });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching show data:', error);
|
||||
logger.error('Error fetching show data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import Animated, {
|
|||
} from 'react-native-reanimated';
|
||||
import { torrentService } 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 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]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
console.log('StreamCard pressed:', {
|
||||
logger.log('StreamCard pressed:', {
|
||||
isTorrent,
|
||||
isDebrid,
|
||||
hasProgress: !!torrentProgress,
|
||||
|
|
@ -310,7 +311,7 @@ export const StreamsScreen = () => {
|
|||
// Monitor streams loading start
|
||||
useEffect(() => {
|
||||
if (loadingStreams || loadingEpisodeStreams) {
|
||||
console.log("⏱️ Stream loading started");
|
||||
logger.log("⏱️ Stream loading started");
|
||||
const now = Date.now();
|
||||
setLoadStartTime(now);
|
||||
setProviderLoadTimes({});
|
||||
|
|
@ -368,7 +369,7 @@ export const StreamsScreen = () => {
|
|||
// Update provider status when new streams appear
|
||||
setProviderStatus(prev => {
|
||||
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
|
||||
if (prev[parentProvider]?.loading) {
|
||||
|
|
@ -409,7 +410,7 @@ export const StreamsScreen = () => {
|
|||
timeCompleted: Date.now()
|
||||
};
|
||||
updated = true;
|
||||
console.log(`⚠️ Provider "${provider}" timed out or failed`);
|
||||
logger.log(`⚠️ Provider "${provider}" timed out or failed`);
|
||||
|
||||
// Update the simpler loading state
|
||||
setLoadingProviders((prevLoading: {[key: string]: boolean}) => ({...prevLoading, [provider]: false}));
|
||||
|
|
@ -423,7 +424,7 @@ export const StreamsScreen = () => {
|
|||
|
||||
React.useEffect(() => {
|
||||
if (type === 'series' && episodeId) {
|
||||
console.log(`🎬 Loading episode streams for: ${episodeId}`);
|
||||
logger.log(`🎬 Loading episode streams for: ${episodeId}`);
|
||||
setLoadingProviders({
|
||||
'source_1': true,
|
||||
'source_2': true,
|
||||
|
|
@ -432,7 +433,7 @@ export const StreamsScreen = () => {
|
|||
setSelectedEpisode(episodeId);
|
||||
loadEpisodeStreams(episodeId);
|
||||
} else if (type === 'movie') {
|
||||
console.log(`🎬 Loading movie streams for: ${id}`);
|
||||
logger.log(`🎬 Loading movie streams for: ${id}`);
|
||||
setLoadingProviders({
|
||||
'source_1': true,
|
||||
'source_2': true,
|
||||
|
|
@ -506,7 +507,7 @@ export const StreamsScreen = () => {
|
|||
const handleStreamPress = useCallback(async (stream: Stream) => {
|
||||
try {
|
||||
if (stream.url) {
|
||||
console.log('handleStreamPress called with stream:', {
|
||||
logger.log('handleStreamPress called with stream:', {
|
||||
url: stream.url,
|
||||
behaviorHints: stream.behaviorHints,
|
||||
isMagnet: stream.url.startsWith('magnet:'),
|
||||
|
|
@ -518,7 +519,7 @@ export const StreamsScreen = () => {
|
|||
const isMagnet = stream.url.startsWith('magnet:') || stream.behaviorHints?.isMagnetStream;
|
||||
|
||||
if (isMagnet) {
|
||||
console.log('Handling magnet link...');
|
||||
logger.log('Handling magnet link...');
|
||||
// Check if there's already an active torrent
|
||||
if (activeTorrent && activeTorrent !== stream.url) {
|
||||
Alert.alert(
|
||||
|
|
@ -533,7 +534,7 @@ export const StreamsScreen = () => {
|
|||
text: 'Stop and Switch',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
console.log('Stopping current torrent and starting new one');
|
||||
logger.log('Stopping current torrent and starting new one');
|
||||
await torrentService.stopStreamAndWait();
|
||||
setActiveTorrent(null);
|
||||
setTorrentProgress({});
|
||||
|
|
@ -545,14 +546,14 @@ export const StreamsScreen = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log('Starting torrent stream...');
|
||||
logger.log('Starting torrent stream...');
|
||||
startTorrentStream(stream);
|
||||
} else {
|
||||
console.log('Playing regular stream...');
|
||||
logger.log('Playing regular stream...');
|
||||
|
||||
// Check if external player is enabled in settings
|
||||
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
|
||||
try {
|
||||
const videoPlayerService = VideoPlayerService;
|
||||
|
|
@ -564,7 +565,7 @@ export const StreamsScreen = () => {
|
|||
releaseDate: metadata?.year?.toString(),
|
||||
});
|
||||
} catch (externalPlayerError) {
|
||||
console.error('External player error:', externalPlayerError);
|
||||
logger.error('External player error:', externalPlayerError);
|
||||
// Fallback to built-in player if external player fails
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
|
|
@ -599,7 +600,7 @@ export const StreamsScreen = () => {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Stream error:', error);
|
||||
logger.error('Stream error:', error);
|
||||
Alert.alert(
|
||||
'Playback Error',
|
||||
error instanceof Error ? error.message : 'An error occurred while playing the video'
|
||||
|
|
@ -611,16 +612,16 @@ export const StreamsScreen = () => {
|
|||
React.useEffect(() => {
|
||||
const unsubscribe = navigation.addListener('focus', () => {
|
||||
// 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) {
|
||||
console.log('[StreamsScreen] Playback ended, cleaning up torrent');
|
||||
logger.log('[StreamsScreen] Playback ended, cleaning up torrent');
|
||||
setIsVideoPlaying(false);
|
||||
|
||||
// Clean up the torrent when returning from video player
|
||||
if (activeTorrent) {
|
||||
console.log('[StreamsScreen] Stopping torrent after playback');
|
||||
logger.log('[StreamsScreen] Stopping torrent after playback');
|
||||
torrentService.stopStreamAndWait().catch(error => {
|
||||
console.error('[StreamsScreen] Error during cleanup:', error);
|
||||
logger.error('[StreamsScreen] Error during cleanup:', error);
|
||||
});
|
||||
setActiveTorrent(null);
|
||||
setTorrentProgress({});
|
||||
|
|
@ -630,11 +631,11 @@ export const StreamsScreen = () => {
|
|||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
console.log('[StreamsScreen] Component unmounting, cleaning up torrent');
|
||||
logger.log('[StreamsScreen] Component unmounting, cleaning up torrent');
|
||||
if (activeTorrent) {
|
||||
console.log('[StreamsScreen] Stopping torrent on unmount');
|
||||
logger.log('[StreamsScreen] Stopping torrent on unmount');
|
||||
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;
|
||||
|
||||
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
|
||||
if (activeTorrent && activeTorrent !== stream.url) {
|
||||
|
|
@ -660,11 +661,11 @@ export const StreamsScreen = () => {
|
|||
onProgress: (progress) => {
|
||||
// Check if progress object is valid and has data
|
||||
if (!progress || Object.keys(progress).length === 0) {
|
||||
console.log('[StreamsScreen] Received empty progress object, ignoring');
|
||||
logger.log('[StreamsScreen] Received empty progress object, ignoring');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[StreamsScreen] Torrent progress update:', {
|
||||
logger.log('[StreamsScreen] Torrent progress update:', {
|
||||
url: stream.url,
|
||||
progress,
|
||||
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
|
||||
if (videoPath) {
|
||||
|
|
@ -692,7 +693,7 @@ export const StreamsScreen = () => {
|
|||
|
||||
try {
|
||||
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
|
||||
try {
|
||||
const videoPlayerService = VideoPlayerService;
|
||||
|
|
@ -704,7 +705,7 @@ export const StreamsScreen = () => {
|
|||
releaseDate: metadata?.year?.toString(),
|
||||
});
|
||||
} 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
|
||||
navigation.navigate('Player', {
|
||||
uri: `file://${videoPath}`,
|
||||
|
|
@ -735,11 +736,11 @@ export const StreamsScreen = () => {
|
|||
|
||||
// Note: Cleanup happens in the focus effect when returning from the player
|
||||
} catch (playerError) {
|
||||
console.error('[StreamsScreen] Video player navigation error:', playerError);
|
||||
logger.error('[StreamsScreen] Video player navigation error:', playerError);
|
||||
setIsVideoPlaying(false);
|
||||
|
||||
// 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();
|
||||
setActiveTorrent(null);
|
||||
setTorrentProgress({});
|
||||
|
|
@ -748,7 +749,7 @@ export const StreamsScreen = () => {
|
|||
}
|
||||
} else {
|
||||
// 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(
|
||||
'Playback Error',
|
||||
'No video file found in torrent'
|
||||
|
|
@ -759,7 +760,7 @@ export const StreamsScreen = () => {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[StreamsScreen] Torrent error:', error);
|
||||
logger.error('[StreamsScreen] Torrent error:', error);
|
||||
// Clean up on error
|
||||
setIsVideoPlaying(false);
|
||||
await torrentService.stopStreamAndWait();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
|||
import { storageService } from '../services/storageService';
|
||||
// Add throttle/debounce imports
|
||||
import { debounce } from 'lodash';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Define the TrackPreferenceType for audio/text tracks
|
||||
type TrackPreferenceType = 'system' | 'disabled' | 'title' | 'language' | 'index';
|
||||
|
|
@ -87,7 +88,7 @@ const VideoPlayer = () => {
|
|||
const uri = __DEV__ && (!routeUri || routeUri.trim() === '') ? developmentTestUrl : routeUri;
|
||||
|
||||
// Log received props for debugging
|
||||
console.log("VideoPlayer received route params:", {
|
||||
logger.log("VideoPlayer received route params:", {
|
||||
uri,
|
||||
title,
|
||||
season,
|
||||
|
|
@ -104,10 +105,10 @@ const VideoPlayer = () => {
|
|||
// Validate URI
|
||||
useEffect(() => {
|
||||
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");
|
||||
} else {
|
||||
console.log("Video URI:", uri);
|
||||
logger.log("Video URI:", uri);
|
||||
}
|
||||
}, [uri]);
|
||||
|
||||
|
|
@ -249,7 +250,7 @@ const VideoPlayer = () => {
|
|||
ScreenOrientation.OrientationLock.LANDSCAPE
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to lock orientation:", error);
|
||||
logger.error("Failed to lock orientation:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -258,7 +259,7 @@ const VideoPlayer = () => {
|
|||
try {
|
||||
await ScreenOrientation.unlockAsync();
|
||||
} 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[] }>) => {
|
||||
console.log("Detected Text Tracks:", e.textTracks);
|
||||
logger.log("Detected Text Tracks:", e.textTracks);
|
||||
setTextTracks(e.textTracks || []);
|
||||
};
|
||||
|
||||
|
|
@ -486,13 +487,13 @@ const VideoPlayer = () => {
|
|||
const cycleAspectRatio = () => {
|
||||
const currentIndex = resizeModes.indexOf(resizeMode);
|
||||
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]);
|
||||
};
|
||||
|
||||
// Function for Back button
|
||||
const handleBackPress = () => {
|
||||
console.log("Close button pressed");
|
||||
logger.log("Close button pressed");
|
||||
|
||||
// Pause video before leaving
|
||||
setPaused(true);
|
||||
|
|
@ -515,7 +516,7 @@ const VideoPlayer = () => {
|
|||
}, 350); // Increase delay to ensure orientation reset completes
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error resetting orientation:", error);
|
||||
logger.error("Error resetting orientation:", error);
|
||||
// Navigate back anyway after a short delay
|
||||
disableImmersiveMode(); // Try disabling again
|
||||
setTimeout(() => {
|
||||
|
|
@ -683,11 +684,11 @@ const VideoPlayer = () => {
|
|||
renderToHardwareTextureAndroid={true}
|
||||
|
||||
onBuffer={(buffer) => {
|
||||
console.log('Buffering:', buffer.isBuffering);
|
||||
logger.log('Buffering:', buffer.isBuffering);
|
||||
}}
|
||||
|
||||
onError={(error) => {
|
||||
console.error('Video playback error:', error);
|
||||
logger.error('Video playback error:', error);
|
||||
alert(`Video Error: ${error.error.errorString} (Code: ${error.error.errorCode})`);
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ class CacheService {
|
|||
}
|
||||
|
||||
public cacheMetadataScreen(id: string, type: string, data: any) {
|
||||
if (!id || !type) return;
|
||||
|
||||
const key = `${type}:${id}`;
|
||||
|
||||
// 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 (this.metadataScreenCache.size >= this.MAX_METADATA_SCREENS) {
|
||||
const firstKey = this.metadataScreenCache.keys().next().value;
|
||||
this.metadataScreenCache.delete(firstKey);
|
||||
if (firstKey) {
|
||||
this.metadataScreenCache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new item
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { stremioService, Meta, Manifest } from './stremioService';
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import axios from 'axios';
|
||||
import { TMDBService } from './tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export interface StreamingAddon {
|
||||
id: string;
|
||||
|
|
@ -82,7 +83,7 @@ class CatalogService {
|
|||
this.library = JSON.parse(storedLibrary);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load library:', error);
|
||||
logger.error('Failed to load library:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +91,7 @@ class CatalogService {
|
|||
try {
|
||||
await AsyncStorage.setItem(this.LIBRARY_KEY, JSON.stringify(this.library));
|
||||
} 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);
|
||||
}
|
||||
} 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 {
|
||||
await AsyncStorage.setItem(this.RECENT_CONTENT_KEY, JSON.stringify(this.recentContent));
|
||||
} 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) {
|
||||
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) {
|
||||
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)));
|
||||
} catch (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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -281,7 +282,7 @@ class CatalogService {
|
|||
|
||||
return null;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -389,7 +390,7 @@ class CatalogService {
|
|||
results.push(...items);
|
||||
}
|
||||
} 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();
|
||||
console.log('Searching Cinemeta for:', trimmedQuery);
|
||||
logger.log('Searching Cinemeta for:', trimmedQuery);
|
||||
|
||||
const addons = await this.getAllAddons();
|
||||
const results: StreamingContent[] = [];
|
||||
|
|
@ -423,7 +424,7 @@ class CatalogService {
|
|||
const cinemeta = addons.find(addon => addon.id === 'com.linvo.cinemeta');
|
||||
|
||||
if (!cinemeta || !cinemeta.catalogs) {
|
||||
console.error('Cinemeta addon not found');
|
||||
logger.error('Cinemeta addon not found');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -432,7 +433,7 @@ class CatalogService {
|
|||
try {
|
||||
// Direct API call to Cinemeta
|
||||
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 metas = response.data.metas || [];
|
||||
|
|
@ -442,7 +443,7 @@ class CatalogService {
|
|||
results.push(...items);
|
||||
}
|
||||
} 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;
|
||||
} catch (error) {
|
||||
console.error('Error getting Stremio ID:', error);
|
||||
logger.error('Error getting Stremio ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Platform } from 'react-native';
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { parseISO, differenceInHours, isToday, addDays } from 'date-fns';
|
||||
import { stremioService } from './stremioService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Define notification storage keys
|
||||
const NOTIFICATION_STORAGE_KEY = 'stremio-notifications';
|
||||
|
|
@ -92,7 +93,7 @@ class NotificationService {
|
|||
this.settings = { ...DEFAULT_NOTIFICATION_SETTINGS, ...JSON.parse(storedSettings) };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading notification settings:', error);
|
||||
logger.error('Error loading notification settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +101,7 @@ class NotificationService {
|
|||
try {
|
||||
await AsyncStorage.setItem(NOTIFICATION_SETTINGS_KEY, JSON.stringify(this.settings));
|
||||
} 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);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading scheduled notifications:', error);
|
||||
logger.error('Error loading scheduled notifications:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +121,7 @@ class NotificationService {
|
|||
try {
|
||||
await AsyncStorage.setItem(NOTIFICATION_STORAGE_KEY, JSON.stringify(this.scheduledNotifications));
|
||||
} catch (error) {
|
||||
console.error('Error saving scheduled notifications:', error);
|
||||
logger.error('Error saving scheduled notifications:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +185,7 @@ class NotificationService {
|
|||
|
||||
return notificationId;
|
||||
} catch (error) {
|
||||
console.error('Error scheduling notification:', error);
|
||||
logger.error('Error scheduling notification:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -219,7 +220,7 @@ class NotificationService {
|
|||
// Save updated list
|
||||
await this.saveScheduledNotifications();
|
||||
} catch (error) {
|
||||
console.error('Error canceling notification:', error);
|
||||
logger.error('Error canceling notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +230,7 @@ class NotificationService {
|
|||
this.scheduledNotifications = [];
|
||||
await this.saveScheduledNotifications();
|
||||
} 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);
|
||||
} catch (error) {
|
||||
console.error(`Error updating notifications for series ${seriesId}:`, error);
|
||||
logger.error(`Error updating notifications for series ${seriesId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
interface WatchProgress {
|
||||
currentTime: number;
|
||||
|
|
@ -33,7 +34,7 @@ class StorageService {
|
|||
const key = this.getWatchProgressKey(id, type, episodeId);
|
||||
await AsyncStorage.setItem(key, JSON.stringify(progress));
|
||||
} 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);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch (error) {
|
||||
console.error('Error getting watch progress:', error);
|
||||
logger.error('Error getting watch progress:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +62,7 @@ class StorageService {
|
|||
const key = this.getWatchProgressKey(id, type, episodeId);
|
||||
await AsyncStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
console.error('Error removing watch progress:', error);
|
||||
logger.error('Error removing watch progress:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ class StorageService {
|
|||
return acc;
|
||||
}, {} as Record<string, WatchProgress>);
|
||||
} catch (error) {
|
||||
console.error('Error getting all watch progress:', error);
|
||||
logger.error('Error getting all watch progress:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import axios from 'axios';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Basic types for Stremio
|
||||
export interface Meta {
|
||||
|
|
@ -163,7 +164,7 @@ class StremioService {
|
|||
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize addons:', error);
|
||||
logger.error('Failed to initialize addons:', error);
|
||||
// Install defaults as fallback
|
||||
await this.installDefaultAddons();
|
||||
this.initialized = true;
|
||||
|
|
@ -184,7 +185,7 @@ class StremioService {
|
|||
return await request();
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
console.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}):`, {
|
||||
logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}):`, {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
isAxiosError: error.isAxiosError,
|
||||
|
|
@ -193,7 +194,7 @@ class StremioService {
|
|||
|
||||
if (attempt < retries) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -211,7 +212,7 @@ class StremioService {
|
|||
}
|
||||
await this.saveInstalledAddons();
|
||||
} 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());
|
||||
await AsyncStorage.setItem(this.STORAGE_KEY, JSON.stringify(addonsArray));
|
||||
} catch (error) {
|
||||
console.error('Failed to save addons:', error);
|
||||
logger.error('Failed to save addons:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +249,7 @@ class StremioService {
|
|||
|
||||
return manifest;
|
||||
} 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}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -298,7 +299,7 @@ class StremioService {
|
|||
result[addon.id] = items;
|
||||
}
|
||||
} 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}`;
|
||||
}
|
||||
|
||||
console.log('Addon base URL:', baseUrl);
|
||||
logger.log('Addon base URL:', baseUrl);
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
|
|
@ -379,7 +380,7 @@ class StremioService {
|
|||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch catalog from ${manifest.name}:`, error);
|
||||
logger.error(`Failed to fetch catalog from ${manifest.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -403,7 +404,7 @@ class StremioService {
|
|||
return response.data.meta;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch meta from ${baseUrl}:`, error);
|
||||
logger.warn(`Failed to fetch meta from ${baseUrl}:`, error);
|
||||
continue; // Try next URL
|
||||
}
|
||||
}
|
||||
|
|
@ -432,15 +433,15 @@ class StremioService {
|
|||
return response.data.meta;
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('No metadata found from any addon');
|
||||
logger.warn('No metadata found from any addon');
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error in getMetaDetails:', error);
|
||||
logger.error('Error in getMetaDetails:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -449,7 +450,7 @@ class StremioService {
|
|||
await this.ensureInitialized();
|
||||
|
||||
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[] = [];
|
||||
|
||||
|
|
@ -457,7 +458,7 @@ class StremioService {
|
|||
const streamAddons = addons
|
||||
.filter(addon => {
|
||||
if (!addon.resources) {
|
||||
console.log(`Addon ${addon.id} has no resources`);
|
||||
logger.log(`Addon ${addon.id} has no resources`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -466,16 +467,16 @@ class StremioService {
|
|||
);
|
||||
|
||||
if (!hasStreamResource) {
|
||||
console.log(`Addon ${addon.id} does not support streaming ${type}`);
|
||||
logger.log(`Addon ${addon.id} does not support streaming ${type}`);
|
||||
}
|
||||
|
||||
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) {
|
||||
console.warn('No addons found that can provide streams');
|
||||
logger.warn('No addons found that can provide streams');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -487,7 +488,7 @@ class StremioService {
|
|||
const promise = (async () => {
|
||||
try {
|
||||
if (!addon.url) {
|
||||
console.warn(`Addon ${addon.id} has no URL`);
|
||||
logger.warn(`Addon ${addon.id} has no URL`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -513,7 +514,7 @@ class StremioService {
|
|||
callback(response.data?.streams || null, addon.name, null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get streams from ${addon.name}:`, error);
|
||||
logger.error(`Failed to get streams from ${addon.name}:`, error);
|
||||
if (callback) {
|
||||
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> {
|
||||
if (!addon.url) {
|
||||
console.warn(`Addon ${addon.id} has no URL defined`);
|
||||
logger.warn(`Addon ${addon.id} has no URL defined`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseUrl = this.getAddonBaseURL(addon.url);
|
||||
const url = `${baseUrl}/stream/${type}/${id}.json`;
|
||||
|
||||
console.log(`Fetching streams from URL: ${url}`);
|
||||
logger.log(`Fetching streams from URL: ${url}`);
|
||||
|
||||
try {
|
||||
// Increase timeout for debrid services
|
||||
const timeout = addon.id.toLowerCase().includes('torrentio') ? 30000 : 10000;
|
||||
|
||||
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, {
|
||||
timeout,
|
||||
headers: {
|
||||
|
|
@ -564,7 +565,7 @@ class StremioService {
|
|||
|
||||
if (response.data && response.data.streams && Array.isArray(response.data.streams)) {
|
||||
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 {
|
||||
streams,
|
||||
|
|
@ -572,7 +573,7 @@ class StremioService {
|
|||
addonName: addon.name
|
||||
};
|
||||
} else {
|
||||
console.warn(`Invalid response format from ${addon.id}:`, response.data);
|
||||
logger.warn(`Invalid response format from ${addon.id}:`, response.data);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorDetails = {
|
||||
|
|
@ -585,7 +586,7 @@ class StremioService {
|
|||
status: error.response?.status,
|
||||
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
|
||||
throw new Error(`Failed to fetch streams from ${addon.name}: ${error.message}`);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import axios from 'axios';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// TMDB API configuration
|
||||
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 {
|
||||
private static instance: TMDBService;
|
||||
private static ratingCache: Map<string, number | null> = new Map();
|
||||
|
|
@ -89,7 +106,7 @@ export class TMDBService {
|
|||
});
|
||||
return response.data.results;
|
||||
} catch (error) {
|
||||
console.error('Failed to search TV show:', error);
|
||||
logger.error('Failed to search TV show:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -107,7 +124,7 @@ export class TMDBService {
|
|||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to get TV show details:', error);
|
||||
logger.error('Failed to get TV show details:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +146,7 @@ export class TMDBService {
|
|||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to get episode external IDs:', error);
|
||||
logger.error('Failed to get episode external IDs:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -165,7 +182,7 @@ export class TMDBService {
|
|||
TMDBService.ratingCache.set(cacheKey, rating);
|
||||
return rating;
|
||||
} 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
|
||||
TMDBService.ratingCache.set(cacheKey, null);
|
||||
return null;
|
||||
|
|
@ -220,7 +237,7 @@ export class TMDBService {
|
|||
|
||||
return season;
|
||||
} catch (error) {
|
||||
console.error('Failed to get season details:', error);
|
||||
logger.error('Failed to get season details:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -245,7 +262,7 @@ export class TMDBService {
|
|||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to get episode details:', error);
|
||||
logger.error('Failed to get episode details:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -264,7 +281,7 @@ export class TMDBService {
|
|||
const tmdbId = await this.findTMDBIdByIMDB(imdbId);
|
||||
return tmdbId;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -297,7 +314,7 @@ export class TMDBService {
|
|||
|
||||
return null;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -334,7 +351,7 @@ export class TMDBService {
|
|||
await Promise.all(seasonPromises);
|
||||
return allEpisodes;
|
||||
} catch (error) {
|
||||
console.error('Failed to get all episodes:', error);
|
||||
logger.error('Failed to get all episodes:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
@ -395,7 +412,7 @@ export class TMDBService {
|
|||
crew: response.data.crew || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch credits:', error);
|
||||
logger.error('Failed to fetch credits:', error);
|
||||
return { cast: [], crew: [] };
|
||||
}
|
||||
}
|
||||
|
|
@ -410,7 +427,7 @@ export class TMDBService {
|
|||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch person details:', error);
|
||||
logger.error('Failed to fetch person details:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -428,14 +445,14 @@ export class TMDBService {
|
|||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to get show external IDs:', error);
|
||||
logger.error('Failed to get show external IDs:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getRecommendations(type: 'movie' | 'tv', tmdbId: string): Promise<any[]> {
|
||||
if (!API_KEY) {
|
||||
console.error('TMDB API key not set');
|
||||
logger.error('TMDB API key not set');
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
|
|
@ -445,7 +462,7 @@ export class TMDBService {
|
|||
});
|
||||
return response.data.results || [];
|
||||
} 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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -463,7 +480,7 @@ export class TMDBService {
|
|||
});
|
||||
return response.data.results;
|
||||
} catch (error) {
|
||||
console.error('Failed to search multi:', error);
|
||||
logger.error('Failed to search multi:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -476,7 +493,7 @@ export class TMDBService {
|
|||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching movie details:', error);
|
||||
logger.error('Error fetching movie details:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -507,10 +524,53 @@ export class TMDBService {
|
|||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching certification:', error);
|
||||
logger.error('Error fetching certification:', error);
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import { NativeModules, NativeEventEmitter, EmitterSubscription, Platform } from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Mock implementation for Expo environment
|
||||
const MockTorrentStreamModule = {
|
||||
TORRENT_PROGRESS_EVENT: 'torrentProgress',
|
||||
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 `https://mock-torrent-stream.com/${magnetUri.substring(0, 10)}.mp4`;
|
||||
},
|
||||
stopStream: () => {
|
||||
console.log('[MockTorrentService] Stopping mock stream');
|
||||
logger.log('[MockTorrentService] Stopping mock stream');
|
||||
},
|
||||
fileExists: async (path: string): Promise<boolean> => {
|
||||
console.log('[MockTorrentService] Checking if file exists:', path);
|
||||
logger.log('[MockTorrentService] Checking if file exists:', path);
|
||||
return false;
|
||||
},
|
||||
// Add these methods to satisfy NativeModule interface
|
||||
|
|
@ -93,11 +94,11 @@ class TorrentService {
|
|||
if (cacheData) {
|
||||
const cacheMap = JSON.parse(cacheData);
|
||||
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;
|
||||
} catch (error) {
|
||||
console.error('[TorrentService] Error loading cache:', error);
|
||||
logger.error('[TorrentService] Error loading cache:', error);
|
||||
this.initialized = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -106,9 +107,9 @@ class TorrentService {
|
|||
try {
|
||||
const cacheData = Object.fromEntries(this.cachedTorrents);
|
||||
await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
|
||||
console.log('[TorrentService] Saved cache mapping');
|
||||
logger.log('[TorrentService] Saved cache mapping');
|
||||
} 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
|
||||
const cachedPath = this.cachedTorrents.get(magnetUri);
|
||||
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
|
||||
if (!TorrentStreamModule) {
|
||||
|
|
@ -141,7 +142,7 @@ class TorrentService {
|
|||
try {
|
||||
const exists = await TorrentStreamModule.fileExists(cachedPath);
|
||||
if (exists) {
|
||||
console.log('[TorrentService] Using cached torrent file');
|
||||
logger.log('[TorrentService] Using cached torrent file');
|
||||
|
||||
// Setup progress listener if callback provided
|
||||
this.setupProgressListener(events);
|
||||
|
|
@ -150,12 +151,12 @@ class TorrentService {
|
|||
await TorrentStreamModule.startStream(magnetUri);
|
||||
return cachedPath;
|
||||
} 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);
|
||||
await this.saveCache();
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
|
@ -168,7 +169,7 @@ class TorrentService {
|
|||
|
||||
// If we're in mock mode (Expo), simulate progress
|
||||
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`;
|
||||
|
||||
// Save to cache
|
||||
|
|
@ -185,19 +186,19 @@ class TorrentService {
|
|||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Save to cache
|
||||
if (filePath) {
|
||||
console.log('[TorrentService] Adding path to cache:', filePath);
|
||||
logger.log('[TorrentService] Adding path to cache:', filePath);
|
||||
this.cachedTorrents.set(magnetUri, filePath);
|
||||
await this.saveCache();
|
||||
}
|
||||
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
console.error('[TorrentService] Error starting torrent stream:', error);
|
||||
logger.error('[TorrentService] Error starting torrent stream:', error);
|
||||
this.cleanup(); // Clean up on error
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -205,18 +206,18 @@ class TorrentService {
|
|||
|
||||
private setupProgressListener(events?: TorrentStreamEvents) {
|
||||
if (events?.onProgress) {
|
||||
console.log('[TorrentService] Setting up progress listener');
|
||||
logger.log('[TorrentService] Setting up progress listener');
|
||||
this.progressListener = this.eventEmitter.addListener(
|
||||
TorrentService.TORRENT_PROGRESS_EVENT,
|
||||
(progress) => {
|
||||
console.log('[TorrentService] Progress event received:', progress);
|
||||
logger.log('[TorrentService] Progress event received:', progress);
|
||||
if (events.onProgress) {
|
||||
events.onProgress(progress);
|
||||
}
|
||||
}
|
||||
);
|
||||
} 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> {
|
||||
console.log('[TorrentService] Stopping stream and waiting for cleanup');
|
||||
logger.log('[TorrentService] Stopping stream and waiting for cleanup');
|
||||
this.cleanup();
|
||||
|
||||
if (TorrentStreamModule) {
|
||||
|
|
@ -270,35 +271,35 @@ class TorrentService {
|
|||
// Wait a moment to ensure native side has cleaned up
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
} catch (error) {
|
||||
console.error('[TorrentService] Error stopping torrent stream:', error);
|
||||
logger.error('[TorrentService] Error stopping torrent stream:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public stopStream(): void {
|
||||
try {
|
||||
console.log('[TorrentService] Stopping stream and cleaning up');
|
||||
logger.log('[TorrentService] Stopping stream and cleaning up');
|
||||
this.cleanup();
|
||||
|
||||
if (TorrentStreamModule) {
|
||||
TorrentStreamModule.stopStream();
|
||||
}
|
||||
} 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
|
||||
this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (this.progressListener) {
|
||||
try {
|
||||
this.progressListener.remove();
|
||||
} catch (error) {
|
||||
console.error('[TorrentService] Error removing progress listener:', error);
|
||||
logger.error('[TorrentService] Error removing progress listener:', error);
|
||||
} finally {
|
||||
this.progressListener = null;
|
||||
}
|
||||
|
|
|
|||
40
src/utils/logger.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
class Logger {
|
||||
private isEnabled: boolean;
|
||||
|
||||
constructor() {
|
||||
// __DEV__ is a global variable in React Native
|
||||
this.isEnabled = __DEV__;
|
||||
}
|
||||
|
||||
log(...args: any[]) {
|
||||
if (this.isEnabled) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
error(...args: any[]) {
|
||||
if (this.isEnabled) {
|
||||
console.error(...args);
|
||||
}
|
||||
}
|
||||
|
||||
warn(...args: any[]) {
|
||||
if (this.isEnabled) {
|
||||
console.warn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
info(...args: any[]) {
|
||||
if (this.isEnabled) {
|
||||
console.info(...args);
|
||||
}
|
||||
}
|
||||
|
||||
debug(...args: any[]) {
|
||||
if (this.isEnabled) {
|
||||
console.debug(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
||||