diff --git a/README.md b/README.md
index 671cef36..dca575ef 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-

+
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 04e1b814..ce1d3fe4 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -95,8 +95,8 @@ android {
applicationId 'com.nuvio.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 35
- versionName "1.3.7"
+ versionCode 36
+ versionName "1.4.0"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
@@ -118,7 +118,7 @@ android {
def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
applicationVariants.all { variant ->
variant.outputs.each { output ->
- def baseVersionCode = 35 // Current versionCode 35 from defaultConfig
+ def baseVersionCode = 36 // Current versionCode 36 from defaultConfig
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
def versionCode = baseVersionCode * 100 // Base multiplier
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 9b0596a4..0c2f2351 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -3,5 +3,5 @@
contain
false
dark
-
1.3.7
+
1.4.0
\ No newline at end of file
diff --git a/app.json b/app.json
index f1007308..4fd63fba 100644
--- a/app.json
+++ b/app.json
@@ -2,7 +2,7 @@
"expo": {
"name": "Nuvio",
"slug": "nuvio",
- "version": "1.3.7",
+ "version": "1.4.0",
"orientation": "default",
"backgroundColor": "#020404",
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
@@ -17,7 +17,7 @@
"ios": {
"supportsTablet": true,
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
- "buildNumber": "35",
+ "buildNumber": "36",
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
@@ -52,7 +52,7 @@
"android.permission.WRITE_SETTINGS"
],
"package": "com.nuvio.app",
- "versionCode": 35,
+ "versionCode": 36,
"architectures": [
"arm64-v8a",
"armeabi-v7a",
@@ -105,6 +105,6 @@
"fallbackToCacheTimeout": 30000,
"url": "https://ota.nuvioapp.space/api/manifest"
},
- "runtimeVersion": "1.3.7"
+ "runtimeVersion": "1.4.0"
}
}
diff --git a/assets/AppIcons/android/mipmap-hdpi/ic_launcher.png b/assets/AppIcons/android/mipmap-hdpi/ic_launcher.png
index fc396770..589a0139 100644
Binary files a/assets/AppIcons/android/mipmap-hdpi/ic_launcher.png and b/assets/AppIcons/android/mipmap-hdpi/ic_launcher.png differ
diff --git a/assets/AppIcons/android/mipmap-mdpi/ic_launcher.png b/assets/AppIcons/android/mipmap-mdpi/ic_launcher.png
index 02d4de67..3a9261d7 100644
Binary files a/assets/AppIcons/android/mipmap-mdpi/ic_launcher.png and b/assets/AppIcons/android/mipmap-mdpi/ic_launcher.png differ
diff --git a/assets/AppIcons/android/mipmap-xhdpi/ic_launcher.png b/assets/AppIcons/android/mipmap-xhdpi/ic_launcher.png
index 41b56cf1..3c101f3f 100644
Binary files a/assets/AppIcons/android/mipmap-xhdpi/ic_launcher.png and b/assets/AppIcons/android/mipmap-xhdpi/ic_launcher.png differ
diff --git a/assets/AppIcons/android/mipmap-xxhdpi/ic_launcher.png b/assets/AppIcons/android/mipmap-xxhdpi/ic_launcher.png
index 570413cc..4a50f3a8 100644
Binary files a/assets/AppIcons/android/mipmap-xxhdpi/ic_launcher.png and b/assets/AppIcons/android/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/assets/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png b/assets/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png
index a101d4a9..c72a7215 100644
Binary files a/assets/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png and b/assets/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/assets/android/ic_launcher-web.png b/assets/android/ic_launcher-web.png
index 130efa79..b2f544c0 100644
Binary files a/assets/android/ic_launcher-web.png and b/assets/android/ic_launcher-web.png differ
diff --git a/assets/android/mipmap-hdpi/ic_launcher.png b/assets/android/mipmap-hdpi/ic_launcher.png
index c304d0eb..d044d633 100644
Binary files a/assets/android/mipmap-hdpi/ic_launcher.png and b/assets/android/mipmap-hdpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-hdpi/ic_launcher_foreground.png b/assets/android/mipmap-hdpi/ic_launcher_foreground.png
index b6d88502..0d8e9ca8 100644
Binary files a/assets/android/mipmap-hdpi/ic_launcher_foreground.png and b/assets/android/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-hdpi/ic_launcher_round.png b/assets/android/mipmap-hdpi/ic_launcher_round.png
index c304d0eb..589a0139 100644
Binary files a/assets/android/mipmap-hdpi/ic_launcher_round.png and b/assets/android/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/assets/android/mipmap-ldpi/ic_launcher.png b/assets/android/mipmap-ldpi/ic_launcher.png
index fa0c0fdd..7912de17 100644
Binary files a/assets/android/mipmap-ldpi/ic_launcher.png and b/assets/android/mipmap-ldpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-ldpi/ic_launcher_foreground.png b/assets/android/mipmap-ldpi/ic_launcher_foreground.png
index 1a8d780b..efd269fb 100644
Binary files a/assets/android/mipmap-ldpi/ic_launcher_foreground.png and b/assets/android/mipmap-ldpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-ldpi/ic_launcher_round.png b/assets/android/mipmap-ldpi/ic_launcher_round.png
index fa0c0fdd..1a964dbd 100644
Binary files a/assets/android/mipmap-ldpi/ic_launcher_round.png and b/assets/android/mipmap-ldpi/ic_launcher_round.png differ
diff --git a/assets/android/mipmap-mdpi/ic_launcher.png b/assets/android/mipmap-mdpi/ic_launcher.png
index 6605b555..526eaffd 100644
Binary files a/assets/android/mipmap-mdpi/ic_launcher.png and b/assets/android/mipmap-mdpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-mdpi/ic_launcher_foreground.png b/assets/android/mipmap-mdpi/ic_launcher_foreground.png
index 0fd39bfd..335c655b 100644
Binary files a/assets/android/mipmap-mdpi/ic_launcher_foreground.png and b/assets/android/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-mdpi/ic_launcher_round.png b/assets/android/mipmap-mdpi/ic_launcher_round.png
index 6605b555..3a9261d7 100644
Binary files a/assets/android/mipmap-mdpi/ic_launcher_round.png and b/assets/android/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/assets/android/mipmap-xhdpi/ic_launcher.png b/assets/android/mipmap-xhdpi/ic_launcher.png
index 0587b0f6..3c101f3f 100644
Binary files a/assets/android/mipmap-xhdpi/ic_launcher.png and b/assets/android/mipmap-xhdpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-xhdpi/ic_launcher_foreground.png b/assets/android/mipmap-xhdpi/ic_launcher_foreground.png
index 98921102..f964ed8d 100644
Binary files a/assets/android/mipmap-xhdpi/ic_launcher_foreground.png and b/assets/android/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-xhdpi/ic_launcher_round.png b/assets/android/mipmap-xhdpi/ic_launcher_round.png
index 0587b0f6..3c101f3f 100644
Binary files a/assets/android/mipmap-xhdpi/ic_launcher_round.png and b/assets/android/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/assets/android/mipmap-xxhdpi/ic_launcher.png b/assets/android/mipmap-xxhdpi/ic_launcher.png
index df92e92b..4a50f3a8 100644
Binary files a/assets/android/mipmap-xxhdpi/ic_launcher.png and b/assets/android/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png b/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png
index 1be0f327..0e2377c7 100644
Binary files a/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png and b/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-xxhdpi/ic_launcher_round.png b/assets/android/mipmap-xxhdpi/ic_launcher_round.png
index df92e92b..4a50f3a8 100644
Binary files a/assets/android/mipmap-xxhdpi/ic_launcher_round.png and b/assets/android/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/assets/android/mipmap-xxxhdpi/ic_launcher.png b/assets/android/mipmap-xxxhdpi/ic_launcher.png
index 947a0bdd..c72a7215 100644
Binary files a/assets/android/mipmap-xxxhdpi/ic_launcher.png and b/assets/android/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png
index dbb45959..dd2e6bee 100644
Binary files a/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png and b/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-xxxhdpi/ic_launcher_round.png b/assets/android/mipmap-xxxhdpi/ic_launcher_round.png
index 947a0bdd..c72a7215 100644
Binary files a/assets/android/mipmap-xxxhdpi/ic_launcher_round.png and b/assets/android/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/assets/android/playstore-icon.png b/assets/android/playstore-icon.png
index 130efa79..b2f544c0 100644
Binary files a/assets/android/playstore-icon.png and b/assets/android/playstore-icon.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png
index 812a1b4d..1fdc796e 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png
index bd953ebc..35572c94 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png
index ddf78c6e..92d5c55a 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png
index 8deccd3e..8954f0f4 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png
index 6ef6611a..37175fd9 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png
index 0a314649..77d6dc8d 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png
index bd953ebc..330fb482 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png
index b1c0ba2a..2cda25b7 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png
index a18395f6..3678d9c9 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png
index a18395f6..3678d9c9 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png
index 262b4f38..7e8b1ce9 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png
index 46ea3f49..52b302e8 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png
index 2067acf4..7e342789 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 5e270ab1..48877c47 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png b/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png
index 6f5a8d75..8c09f13f 100644
Binary files a/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png and b/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png differ
diff --git a/assets/ios/iTunesArtwork@1x.png b/assets/ios/iTunesArtwork@1x.png
index 130efa79..95e87d0b 100644
Binary files a/assets/ios/iTunesArtwork@1x.png and b/assets/ios/iTunesArtwork@1x.png differ
diff --git a/assets/ios/iTunesArtwork@2x.png b/assets/ios/iTunesArtwork@2x.png
index 6f5a8d75..8c09f13f 100644
Binary files a/assets/ios/iTunesArtwork@2x.png and b/assets/ios/iTunesArtwork@2x.png differ
diff --git a/assets/ios/iTunesArtwork@3x.png b/assets/ios/iTunesArtwork@3x.png
index 9fc0d4ad..ce974813 100644
Binary files a/assets/ios/iTunesArtwork@3x.png and b/assets/ios/iTunesArtwork@3x.png differ
diff --git a/eas.json b/eas.json
index b208a76d..d68dbe69 100644
--- a/eas.json
+++ b/eas.json
@@ -5,13 +5,22 @@
},
"build": {
"development": {
+ "env": {
+ "SENTRY_DISABLE_AUTO_UPLOAD": "true"
+ },
"developmentClient": true,
"distribution": "internal"
},
"preview": {
+ "env": {
+ "SENTRY_DISABLE_AUTO_UPLOAD": "true"
+ },
"distribution": "internal"
},
"production": {
+ "env": {
+ "SENTRY_DISABLE_AUTO_UPLOAD": "true"
+ },
"autoIncrement": true,
"extends": "apk",
"android": {
@@ -21,12 +30,18 @@
}
},
"release": {
+ "env": {
+ "SENTRY_DISABLE_AUTO_UPLOAD": "true"
+ },
"distribution": "store",
"android": {
"buildType": "app-bundle"
}
},
"apk": {
+ "env": {
+ "SENTRY_DISABLE_AUTO_UPLOAD": "true"
+ },
"android": {
"buildType": "apk",
"gradleCommand": ":app:assembleRelease"
diff --git a/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png
index 28ac68d9..9adedac5 100644
Binary files a/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png and b/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ
diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist
index 6b345fac..ba093107 100644
--- a/ios/Nuvio/Info.plist
+++ b/ios/Nuvio/Info.plist
@@ -39,7 +39,7 @@
CFBundleVersion
-
35
+
36
LSMinimumSystemVersion
12.0
LSRequiresIPhoneOS
diff --git a/package.json b/package.json
index e5f102df..8bff9af6 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
- "build": "export NODE_ENV=production && cd android && ./gradlew assembleRelease",
+ "build": "export NODE_ENV=production && export SENTRY_DISABLE_AUTO_UPLOAD=true && cd android && ./gradlew assembleRelease",
"postinstall": "patch-package"
},
"dependencies": {
diff --git a/src/assets/splash-icon-new.png b/src/assets/splash-icon-new.png
index 8699fcd1..d0bbb55f 100644
Binary files a/src/assets/splash-icon-new.png and b/src/assets/splash-icon-new.png differ
diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts
index 48cbdd72..3773150d 100644
--- a/src/hooks/useMetadata.ts
+++ b/src/hooks/useMetadata.ts
@@ -734,44 +734,119 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
let contentResult = null;
let lastError = null;
- // Try with original ID first
- try {
- console.log('🔍 [useMetadata] Attempting metadata fetch with original ID:', { type, actualId, addonId });
- const [content, castData] = await Promise.allSettled([
- // Load content with timeout and retry
- withRetry(async () => {
- console.log('🔍 [useMetadata] Calling catalogService.getEnhancedContentDetails:', { type, actualId, addonId });
- const result = await withTimeout(
- catalogService.getEnhancedContentDetails(type, actualId, addonId),
- API_TIMEOUT
- );
- // Store the actual ID used (could be IMDB)
- if (actualId.startsWith('tt')) {
- setImdbId(actualId);
- }
- console.log('🔍 [useMetadata] catalogService.getEnhancedContentDetails result:', {
- hasResult: Boolean(result),
- resultId: result?.id,
- resultName: result?.name,
- resultType: result?.type
- });
- if (__DEV__) logger.log('[loadMetadata] addon metadata fetched', { hasResult: Boolean(result) });
- return result;
- }),
- // Start loading cast immediately in parallel
- loadCast()
- ]);
+ // Check if user prefers external meta addons
+ const preferExternal = settings.preferExternalMetaAddonDetail;
- contentResult = content;
- if (content.status === 'fulfilled' && content.value) {
- console.log('🔍 [useMetadata] Successfully got metadata with original ID');
- } else {
- console.log('🔍 [useMetadata] Original ID failed, will try fallback conversion');
- lastError = (content as any)?.reason;
+ if (preferExternal) {
+ // Try external meta addons first
+ try {
+ console.log('🔍 [useMetadata] Trying external meta addons first');
+ const [content, castData] = await Promise.allSettled([
+ withRetry(async () => {
+ // Get all installed addons
+ const allAddons = await stremioService.getInstalledAddonsAsync();
+
+ // Find catalog addon index
+ const catalogAddonIndex = allAddons.findIndex(addon => addon.id === addonId);
+
+ // Filter for meta addons that are BEFORE catalog addon in priority
+ const externalMetaAddons = allAddons
+ .slice(0, catalogAddonIndex >= 0 ? catalogAddonIndex : allAddons.length)
+ .filter(addon => {
+ if (!addon.resources || !Array.isArray(addon.resources)) return false;
+
+ return addon.resources.some(resource => {
+ if (typeof resource === 'string') return resource === 'meta';
+ return (resource as any).name === 'meta';
+ });
+ });
+
+ // Try each external meta addon in priority order
+ for (const addon of externalMetaAddons) {
+ try {
+ const result = await withTimeout(
+ stremioService.getMetaDetails(type, actualId, addon.id),
+ API_TIMEOUT
+ );
+
+ if (result) {
+ console.log('🔍 [useMetadata] Got metadata from external addon:', addon.name);
+ if (actualId.startsWith('tt')) {
+ setImdbId(actualId);
+ }
+ return result;
+ }
+ } catch (error) {
+ console.log('🔍 [useMetadata] External addon failed:', addon.name, error);
+ continue;
+ }
+ }
+
+ // If no external addon worked, fall back to catalog addon
+ console.log('🔍 [useMetadata] No external meta addon worked, falling back to catalog addon');
+ const result = await withTimeout(
+ catalogService.getEnhancedContentDetails(type, actualId, addonId),
+ API_TIMEOUT
+ );
+ if (actualId.startsWith('tt')) {
+ setImdbId(actualId);
+ }
+ return result;
+ }),
+ loadCast()
+ ]);
+
+ contentResult = content;
+ if (content.status === 'fulfilled' && content.value) {
+ console.log('🔍 [useMetadata] Successfully got metadata with external meta addon priority');
+ } else {
+ console.log('🔍 [useMetadata] External meta addon priority failed, will try fallback');
+ lastError = (content as any)?.reason;
+ }
+ } catch (error) {
+ console.log('🔍 [useMetadata] External meta addon attempt failed:', { error: error instanceof Error ? error.message : String(error) });
+ lastError = error;
+ }
+ } else {
+ // Original behavior: try with original ID first
+ try {
+ console.log('🔍 [useMetadata] Attempting metadata fetch with original ID:', { type, actualId, addonId });
+ const [content, castData] = await Promise.allSettled([
+ // Load content with timeout and retry
+ withRetry(async () => {
+ console.log('🔍 [useMetadata] Calling catalogService.getEnhancedContentDetails:', { type, actualId, addonId });
+ const result = await withTimeout(
+ catalogService.getEnhancedContentDetails(type, actualId, addonId),
+ API_TIMEOUT
+ );
+ // Store the actual ID used (could be IMDB)
+ if (actualId.startsWith('tt')) {
+ setImdbId(actualId);
+ }
+ console.log('🔍 [useMetadata] catalogService.getEnhancedContentDetails result:', {
+ hasResult: Boolean(result),
+ resultId: result?.id,
+ resultName: result?.name,
+ resultType: result?.type
+ });
+ if (__DEV__) logger.log('[loadMetadata] addon metadata fetched', { hasResult: Boolean(result) });
+ return result;
+ }),
+ // Start loading cast immediately in parallel
+ loadCast()
+ ]);
+
+ contentResult = content;
+ if (content.status === 'fulfilled' && content.value) {
+ console.log('🔍 [useMetadata] Successfully got metadata with original ID');
+ } else {
+ console.log('🔍 [useMetadata] Original ID failed, will try fallback conversion');
+ lastError = (content as any)?.reason;
+ }
+ } catch (error) {
+ console.log('🔍 [useMetadata] Original ID attempt failed:', { error: error instanceof Error ? error.message : String(error) });
+ lastError = error;
}
- } catch (error) {
- console.log('🔍 [useMetadata] Original ID attempt failed:', { error: error instanceof Error ? error.message : String(error) });
- lastError = error;
}
// If original TMDB ID failed and enrichment is disabled, try ID conversion as fallback
@@ -1831,10 +1906,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
seasonNum = parts.pop() || '';
showIdStr = parts.join(':');
} else if (parts.length === 2) {
- // Edge case: maybe just id:episode? unlikely but safe fallback
- episodeNum = parts[1];
- seasonNum = '1'; // Default
+ // For IDs like mal:57658:1, this is showId:episode (no season)
showIdStr = parts[0];
+ episodeNum = parts[1];
+ seasonNum = ''; // No season for this format
}
if (__DEV__) console.log(`🔍 [loadEpisodeStreams] Parsed ID: show=${showIdStr}, s=${seasonNum}, e=${episodeNum}`);
@@ -1912,6 +1987,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// This handles cases where 'tt' is used for a unique episode ID directly
if (!seasonNum && !episodeNum) {
stremioEpisodeId = episodeId;
+ } else if (!seasonNum) {
+ // No season (e.g., mal:57658:1) - use id:episode format
+ stremioEpisodeId = `${id}:${episodeNum}`;
} else {
stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
}
@@ -1923,6 +2001,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (!seasonNum && !episodeNum) {
// Remove 'series:' prefix if present to be safe, though parsing logic above usually handles it
stremioEpisodeId = episodeId.replace(/^series:/, '');
+ } else if (!seasonNum) {
+ // No season (e.g., mal:57658:1) - use id:episode format
+ stremioEpisodeId = `${id}:${episodeNum}`;
} else {
stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
}
diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
index befff6bb..71093b7e 100644
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -115,6 +115,8 @@ export interface AppSettings {
preferredAudioLanguage: string; // Preferred language for audio tracks (ISO 639-1 code)
subtitleSourcePreference: 'internal' | 'external' | 'any'; // Prefer internal (embedded), external (addon), or any
enableSubtitleAutoSelect: boolean; // Auto-select subtitles based on preferences
+ // External metadata addon preference
+ preferExternalMetaAddonDetail: boolean; // Prefer metadata from external meta addons on detail page
}
export const DEFAULT_SETTINGS: AppSettings = {
@@ -203,6 +205,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
preferredAudioLanguage: 'en', // Default to English audio
subtitleSourcePreference: 'internal', // Prefer internal/embedded subtitles first
enableSubtitleAutoSelect: true, // Auto-select subtitles by default
+ // External metadata addon preference
+ preferExternalMetaAddonDetail: false, // Disabled by default
};
const SETTINGS_STORAGE_KEY = 'app_settings';
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 099177f0..3be593ee 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -983,6 +983,8 @@
"select_catalogs": "Select Catalogs",
"all_catalogs": "All catalogs",
"selected": "selected",
+ "prefer_external_meta": "Prefer External Meta Addon",
+ "prefer_external_meta_desc": "Use external metadata on detail page",
"hero_layout": "Hero Layout",
"layout_legacy": "Legacy",
"layout_carousel": "Carousel",
diff --git a/src/i18n/locales/mk.json b/src/i18n/locales/mk.json
index 37204427..22b8287d 100644
--- a/src/i18n/locales/mk.json
+++ b/src/i18n/locales/mk.json
@@ -1424,4 +1424,5 @@
"no_logs_captured": "Нема снимено логови."
}
}
-}
\ No newline at end of file
+}
+}
diff --git a/src/i18n/locales/nl-NL.json b/src/i18n/locales/nl-NL.json
index ddc97f81..467a6a01 100644
--- a/src/i18n/locales/nl-NL.json
+++ b/src/i18n/locales/nl-NL.json
@@ -625,8 +625,7 @@
"enter_custom_key": "Voer je eigen TMDb API key in en sla op.",
"key_verified": "API key geverifieerd en succesvol opgeslagen."
},
- {
- "settings": {
+ "settings": {
"language": "Taal",
"select_language": "Selecteer taal",
"english": "Engels",
@@ -650,7 +649,7 @@
"slovenian": "Sloveens",
"macedonian": "Macedonisch",
"russian": "Russisch",
- "filipino": "Filipijns"
+ "filipino": "Filipijns",
"dutch_nl": "Nederlands (Nederland)",
"romanian": "Roemeens",
"albanian": "Albanees",
@@ -1029,8 +1028,7 @@
"no_upcoming_found": "Geen aankomende afleveringen gevonden",
"add_series_desc": "Voeg series toe aan je bibliotheek om ze hier te zien"
},
- {
- "mdblist": {
+ "mdblist": {
"title": "Beoordelingsbronnen",
"status_disabled": "MDBList uitgeschakeld",
"status_active": "API-key actief",
@@ -1429,4 +1427,4 @@
"no_logs_captured": "Geen logs vastgelegd."
}
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json
index 184316c0..0b61952a 100644
--- a/src/i18n/locales/pl.json
+++ b/src/i18n/locales/pl.json
@@ -983,6 +983,8 @@
"select_catalogs": "Wybierz katalogi",
"all_catalogs": "Wszystkie katalogi",
"selected": "wybrane",
+ "prefer_external_meta": "Preferuj zewnętrzny dodatek meta",
+ "prefer_external_meta_desc": "Używaj zewnętrznych metadanych na stronie szczegółów",
"hero_layout": "Układ sekcji Hero",
"layout_legacy": "Klasyczny",
"layout_carousel": "Karuzela",
diff --git a/src/i18n/locales/ro.json b/src/i18n/locales/ro.json
index 2fc7c435..34aad5df 100644
--- a/src/i18n/locales/ro.json
+++ b/src/i18n/locales/ro.json
@@ -625,8 +625,7 @@
"enter_custom_key": "Te rugăm să introduci și să salvezi cheia personalizată.",
"key_verified": "Cheia API a fost verificată și salvată cu succes."
},
- {
- "settings": {
+ "settings": {
"language": "Limbă",
"select_language": "Selectează limba",
"english": "Engleză",
@@ -1029,8 +1028,7 @@
"no_upcoming_found": "Niciun episod viitor găsit",
"add_series_desc": "Adaugă seriale în bibliotecă pentru a vedea episoadele lor viitoare aici"
},
- {
- "mdblist": {
+ "mdblist": {
"title": "Surse de evaluare",
"status_disabled": "MDBList dezactivat",
"status_active": "Cheie API activă",
@@ -1429,4 +1427,4 @@
"no_logs_captured": "Niciun log capturat."
}
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/locales/sq.json b/src/i18n/locales/sq.json
index ce1b2de8..8606830e 100644
--- a/src/i18n/locales/sq.json
+++ b/src/i18n/locales/sq.json
@@ -625,8 +625,7 @@
"enter_custom_key": "Ju lutem jepni dhe ruani çelësin tuaj personal.",
"key_verified": "Çelësi API u verifikua dhe u ruajt me sukses."
},
- {
- "settings": {
+ "settings": {
"language": "Gjuha",
"select_language": "Zgjidh Gjuhën",
"english": "Anglisht",
@@ -973,8 +972,7 @@
"alert_disconnect_title": "Shkëput Torbox",
"alert_disconnect_msg": "Jeni të sigurt? Kjo do të fshijë çelësin API të ruajtur."
},
- {
- "home_screen": {
+ "home_screen": {
"title": "Cilësimet e Ekranit Kryesor",
"changes_applied": "Ndryshimet u aplikuan",
"display_options": "OPSIONET E SHFAQJES",
@@ -1429,4 +1427,4 @@
"no_logs_captured": "Nuk u kap asnjë log."
}
}
-}
\ No newline at end of file
+}
diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx
index 373bb512..4033429f 100644
--- a/src/screens/AuthScreen.tsx
+++ b/src/screens/AuthScreen.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Animated, Easing, Keyboard, StatusBar, useWindowDimensions } from 'react-native';
+import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Animated, Easing, Keyboard, StatusBar, useWindowDimensions, Linking } from 'react-native';
import { mmkvStorage } from '../services/mmkvStorage';
import { LinearGradient } from 'expo-linear-gradient';
import { MaterialIcons } from '@expo/vector-icons';
@@ -11,7 +11,7 @@ import { useToast } from '../contexts/ToastContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__';
-const AUTH_BG_GRADIENT = ['#07090F', '#0D1020', '#140B24'];
+const AUTH_BG_GRADIENT = ['#07090F', '#0D1020', '#140B24'] as const;
const normalizeAuthErrorMessage = (input: string): string => {
const raw = (input || '').trim();
@@ -425,6 +425,16 @@ const AuthScreen: React.FC = () => {
+ {mode === 'signin' && (
+
Linking.openURL('https://nuvioapp.space/account/reset-password')}
+ activeOpacity={0.75}
+ style={styles.forgotPasswordButton}
+ >
+ Forgot password?
+
+ )}
+
{/* Confirm Password (signup only) */}
{mode === 'signup' && (
@@ -744,6 +754,15 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: '500',
},
+ forgotPasswordButton: {
+ alignSelf: 'flex-end',
+ marginTop: -6,
+ marginBottom: 12,
+ },
+ forgotPasswordText: {
+ fontSize: 13,
+ fontWeight: '600',
+ },
});
export default AuthScreen;
diff --git a/src/screens/HomeScreenSettings.tsx b/src/screens/HomeScreenSettings.tsx
index 74822d07..7e563916 100644
--- a/src/screens/HomeScreenSettings.tsx
+++ b/src/screens/HomeScreenSettings.tsx
@@ -344,9 +344,22 @@ const HomeScreenSettings: React.FC = () => {
colors={colors}
renderControl={ChevronRight}
onPress={() => navigation.navigate('HeroCatalogs')}
- isLast={true}
/>
)}
+ (
+ handleUpdateSetting('preferExternalMetaAddonDetail', value)}
+ />
+ )}
+ isLast={true}
+ />
{settings.showHeroSection && (
diff --git a/src/services/supabaseSyncService.ts b/src/services/supabaseSyncService.ts
index 2134edbc..6c4265bb 100644
--- a/src/services/supabaseSyncService.ts
+++ b/src/services/supabaseSyncService.ts
@@ -825,7 +825,11 @@ class SupabaseSyncService {
}
private normalizeUrl(url: string): string {
- return url.trim().toLowerCase();
+ let u = url.trim().toLowerCase();
+
+ u = u.replace(/\/manifest\.json\/?$/i, '');
+ u = u.replace(/\/+$/, '');
+ return u;
}
private toBigIntNumber(value: unknown): number {
@@ -1063,14 +1067,37 @@ class SupabaseSyncService {
.map((url) => this.normalizeUrl(url))
);
+ // Build a set of currently-installed addon manifest IDs so we can also
+ // skip by ID (prevents duplicate installations of stream-providing addons
+ // that the URL check alone might miss due to URL format differences).
+ const installedAddonIds = new Set(
+ installed.map((addon) => addon.id).filter(Boolean)
+ );
+
for (const row of rows || []) {
if (!row.url) continue;
const normalized = this.normalizeUrl(row.url);
if (installedUrls.has(normalized)) continue;
try {
+ // Pre-check: fetch manifest to see if this addon ID is already installed.
+ // This prevents creating duplicate installations for stream-providing
+ // addons whose URLs differ only by format (e.g. with/without manifest.json).
+ let manifest: Manifest | null = null;
+ try {
+ manifest = await stremioService.getManifest(row.url);
+ } catch {
+ // If manifest fetch fails, fall through to installAddon which will also fail and be caught below.
+ }
+ if (manifest?.id && installedAddonIds.has(manifest.id)) {
+ // Addon already installed under a different URL variant — skip.
+ logger.log(`[SupabaseSyncService] pullAddonsToLocal: skipping duplicate addon id=${manifest.id} url=${row.url}`);
+ installedUrls.add(normalized);
+ continue;
+ }
await stremioService.installAddon(row.url);
installedUrls.add(normalized);
+ if (manifest?.id) installedAddonIds.add(manifest.id);
} catch (error) {
logger.warn('[SupabaseSyncService] Failed to install synced addon:', row.url, error);
}
diff --git a/src/utils/version.ts b/src/utils/version.ts
index 06f1acea..ce2f67b5 100644
--- a/src/utils/version.ts
+++ b/src/utils/version.ts
@@ -1,7 +1,7 @@
// Single source of truth for the app version displayed in Settings
// Update this when bumping app version
-export const APP_VERSION = '1.3.7';
+export const APP_VERSION = '1.4.0';
export function getDisplayedAppVersion(): string {
return APP_VERSION;