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

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;