Merge branch 'tapframe:main' into main
|
|
@ -1,6 +1,6 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="assets/nuviotext.png" alt="Nuvio" width="300" />
|
<img src="https://github.com/tapframe/NuvioTV/blob/main/assets/brand/app_logo_wordmark.png" alt="Nuvio" width="300" />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,8 +95,8 @@ android {
|
||||||
applicationId 'com.nuvio.app'
|
applicationId 'com.nuvio.app'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 35
|
versionCode 36
|
||||||
versionName "1.3.7"
|
versionName "1.4.0"
|
||||||
|
|
||||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
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]
|
def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
variant.outputs.each { output ->
|
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 abiName = output.getFilter(com.android.build.OutputFile.ABI)
|
||||||
|
|
||||||
def versionCode = baseVersionCode * 100 // Base multiplier
|
def versionCode = baseVersionCode * 100 // Base multiplier
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,5 @@
|
||||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||||
<string name="expo_system_ui_user_interface_style" translatable="false">dark</string>
|
<string name="expo_system_ui_user_interface_style" translatable="false">dark</string>
|
||||||
<string name="expo_runtime_version">1.3.7</string>
|
<string name="expo_runtime_version">1.4.0</string>
|
||||||
</resources>
|
</resources>
|
||||||
8
app.json
|
|
@ -2,7 +2,7 @@
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Nuvio",
|
"name": "Nuvio",
|
||||||
"slug": "nuvio",
|
"slug": "nuvio",
|
||||||
"version": "1.3.7",
|
"version": "1.4.0",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"backgroundColor": "#020404",
|
"backgroundColor": "#020404",
|
||||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||||
"buildNumber": "35",
|
"buildNumber": "36",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NSAppTransportSecurity": {
|
"NSAppTransportSecurity": {
|
||||||
"NSAllowsArbitraryLoads": true
|
"NSAllowsArbitraryLoads": true
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
"android.permission.WRITE_SETTINGS"
|
"android.permission.WRITE_SETTINGS"
|
||||||
],
|
],
|
||||||
"package": "com.nuvio.app",
|
"package": "com.nuvio.app",
|
||||||
"versionCode": 35,
|
"versionCode": 36,
|
||||||
"architectures": [
|
"architectures": [
|
||||||
"arm64-v8a",
|
"arm64-v8a",
|
||||||
"armeabi-v7a",
|
"armeabi-v7a",
|
||||||
|
|
@ -105,6 +105,6 @@
|
||||||
"fallbackToCacheTimeout": 30000,
|
"fallbackToCacheTimeout": 30000,
|
||||||
"url": "https://ota.nuvioapp.space/api/manifest"
|
"url": "https://ota.nuvioapp.space/api/manifest"
|
||||||
},
|
},
|
||||||
"runtimeVersion": "1.3.7"
|
"runtimeVersion": "1.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 133 KiB |
15
eas.json
|
|
@ -5,13 +5,22 @@
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"development": {
|
"development": {
|
||||||
|
"env": {
|
||||||
|
"SENTRY_DISABLE_AUTO_UPLOAD": "true"
|
||||||
|
},
|
||||||
"developmentClient": true,
|
"developmentClient": true,
|
||||||
"distribution": "internal"
|
"distribution": "internal"
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
|
"env": {
|
||||||
|
"SENTRY_DISABLE_AUTO_UPLOAD": "true"
|
||||||
|
},
|
||||||
"distribution": "internal"
|
"distribution": "internal"
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
|
"env": {
|
||||||
|
"SENTRY_DISABLE_AUTO_UPLOAD": "true"
|
||||||
|
},
|
||||||
"autoIncrement": true,
|
"autoIncrement": true,
|
||||||
"extends": "apk",
|
"extends": "apk",
|
||||||
"android": {
|
"android": {
|
||||||
|
|
@ -21,12 +30,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
|
"env": {
|
||||||
|
"SENTRY_DISABLE_AUTO_UPLOAD": "true"
|
||||||
|
},
|
||||||
"distribution": "store",
|
"distribution": "store",
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "app-bundle"
|
"buildType": "app-bundle"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apk": {
|
"apk": {
|
||||||
|
"env": {
|
||||||
|
"SENTRY_DISABLE_AUTO_UPLOAD": "true"
|
||||||
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk",
|
"buildType": "apk",
|
||||||
"gradleCommand": ":app:assembleRelease"
|
"gradleCommand": ":app:assembleRelease"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 125 KiB |
|
|
@ -39,7 +39,7 @@
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>35</string>
|
<string>36</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0</string>
|
<string>12.0</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios",
|
"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"
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 153 KiB |
|
|
@ -734,44 +734,119 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
let contentResult = null;
|
let contentResult = null;
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
|
|
||||||
// Try with original ID first
|
// Check if user prefers external meta addons
|
||||||
try {
|
const preferExternal = settings.preferExternalMetaAddonDetail;
|
||||||
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 (preferExternal) {
|
||||||
if (content.status === 'fulfilled' && content.value) {
|
// Try external meta addons first
|
||||||
console.log('🔍 [useMetadata] Successfully got metadata with original ID');
|
try {
|
||||||
} else {
|
console.log('🔍 [useMetadata] Trying external meta addons first');
|
||||||
console.log('🔍 [useMetadata] Original ID failed, will try fallback conversion');
|
const [content, castData] = await Promise.allSettled([
|
||||||
lastError = (content as any)?.reason;
|
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
|
// 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() || '';
|
seasonNum = parts.pop() || '';
|
||||||
showIdStr = parts.join(':');
|
showIdStr = parts.join(':');
|
||||||
} else if (parts.length === 2) {
|
} else if (parts.length === 2) {
|
||||||
// Edge case: maybe just id:episode? unlikely but safe fallback
|
// For IDs like mal:57658:1, this is showId:episode (no season)
|
||||||
episodeNum = parts[1];
|
|
||||||
seasonNum = '1'; // Default
|
|
||||||
showIdStr = parts[0];
|
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}`);
|
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
|
// This handles cases where 'tt' is used for a unique episode ID directly
|
||||||
if (!seasonNum && !episodeNum) {
|
if (!seasonNum && !episodeNum) {
|
||||||
stremioEpisodeId = episodeId;
|
stremioEpisodeId = episodeId;
|
||||||
|
} else if (!seasonNum) {
|
||||||
|
// No season (e.g., mal:57658:1) - use id:episode format
|
||||||
|
stremioEpisodeId = `${id}:${episodeNum}`;
|
||||||
} else {
|
} else {
|
||||||
stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
|
stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
|
||||||
}
|
}
|
||||||
|
|
@ -1923,6 +2001,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
if (!seasonNum && !episodeNum) {
|
if (!seasonNum && !episodeNum) {
|
||||||
// Remove 'series:' prefix if present to be safe, though parsing logic above usually handles it
|
// Remove 'series:' prefix if present to be safe, though parsing logic above usually handles it
|
||||||
stremioEpisodeId = episodeId.replace(/^series:/, '');
|
stremioEpisodeId = episodeId.replace(/^series:/, '');
|
||||||
|
} else if (!seasonNum) {
|
||||||
|
// No season (e.g., mal:57658:1) - use id:episode format
|
||||||
|
stremioEpisodeId = `${id}:${episodeNum}`;
|
||||||
} else {
|
} else {
|
||||||
stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
|
stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,8 @@ export interface AppSettings {
|
||||||
preferredAudioLanguage: string; // Preferred language for audio tracks (ISO 639-1 code)
|
preferredAudioLanguage: string; // Preferred language for audio tracks (ISO 639-1 code)
|
||||||
subtitleSourcePreference: 'internal' | 'external' | 'any'; // Prefer internal (embedded), external (addon), or any
|
subtitleSourcePreference: 'internal' | 'external' | 'any'; // Prefer internal (embedded), external (addon), or any
|
||||||
enableSubtitleAutoSelect: boolean; // Auto-select subtitles based on preferences
|
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 = {
|
export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
|
|
@ -203,6 +205,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
preferredAudioLanguage: 'en', // Default to English audio
|
preferredAudioLanguage: 'en', // Default to English audio
|
||||||
subtitleSourcePreference: 'internal', // Prefer internal/embedded subtitles first
|
subtitleSourcePreference: 'internal', // Prefer internal/embedded subtitles first
|
||||||
enableSubtitleAutoSelect: true, // Auto-select subtitles by default
|
enableSubtitleAutoSelect: true, // Auto-select subtitles by default
|
||||||
|
// External metadata addon preference
|
||||||
|
preferExternalMetaAddonDetail: false, // Disabled by default
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_STORAGE_KEY = 'app_settings';
|
const SETTINGS_STORAGE_KEY = 'app_settings';
|
||||||
|
|
|
||||||
|
|
@ -983,6 +983,8 @@
|
||||||
"select_catalogs": "Select Catalogs",
|
"select_catalogs": "Select Catalogs",
|
||||||
"all_catalogs": "All catalogs",
|
"all_catalogs": "All catalogs",
|
||||||
"selected": "selected",
|
"selected": "selected",
|
||||||
|
"prefer_external_meta": "Prefer External Meta Addon",
|
||||||
|
"prefer_external_meta_desc": "Use external metadata on detail page",
|
||||||
"hero_layout": "Hero Layout",
|
"hero_layout": "Hero Layout",
|
||||||
"layout_legacy": "Legacy",
|
"layout_legacy": "Legacy",
|
||||||
"layout_carousel": "Carousel",
|
"layout_carousel": "Carousel",
|
||||||
|
|
|
||||||
|
|
@ -1424,4 +1424,5 @@
|
||||||
"no_logs_captured": "Нема снимено логови."
|
"no_logs_captured": "Нема снимено логови."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -625,8 +625,7 @@
|
||||||
"enter_custom_key": "Voer je eigen TMDb API key in en sla op.",
|
"enter_custom_key": "Voer je eigen TMDb API key in en sla op.",
|
||||||
"key_verified": "API key geverifieerd en succesvol opgeslagen."
|
"key_verified": "API key geverifieerd en succesvol opgeslagen."
|
||||||
},
|
},
|
||||||
{
|
"settings": {
|
||||||
"settings": {
|
|
||||||
"language": "Taal",
|
"language": "Taal",
|
||||||
"select_language": "Selecteer taal",
|
"select_language": "Selecteer taal",
|
||||||
"english": "Engels",
|
"english": "Engels",
|
||||||
|
|
@ -650,7 +649,7 @@
|
||||||
"slovenian": "Sloveens",
|
"slovenian": "Sloveens",
|
||||||
"macedonian": "Macedonisch",
|
"macedonian": "Macedonisch",
|
||||||
"russian": "Russisch",
|
"russian": "Russisch",
|
||||||
"filipino": "Filipijns"
|
"filipino": "Filipijns",
|
||||||
"dutch_nl": "Nederlands (Nederland)",
|
"dutch_nl": "Nederlands (Nederland)",
|
||||||
"romanian": "Roemeens",
|
"romanian": "Roemeens",
|
||||||
"albanian": "Albanees",
|
"albanian": "Albanees",
|
||||||
|
|
@ -1029,8 +1028,7 @@
|
||||||
"no_upcoming_found": "Geen aankomende afleveringen gevonden",
|
"no_upcoming_found": "Geen aankomende afleveringen gevonden",
|
||||||
"add_series_desc": "Voeg series toe aan je bibliotheek om ze hier te zien"
|
"add_series_desc": "Voeg series toe aan je bibliotheek om ze hier te zien"
|
||||||
},
|
},
|
||||||
{
|
"mdblist": {
|
||||||
"mdblist": {
|
|
||||||
"title": "Beoordelingsbronnen",
|
"title": "Beoordelingsbronnen",
|
||||||
"status_disabled": "MDBList uitgeschakeld",
|
"status_disabled": "MDBList uitgeschakeld",
|
||||||
"status_active": "API-key actief",
|
"status_active": "API-key actief",
|
||||||
|
|
@ -1429,4 +1427,4 @@
|
||||||
"no_logs_captured": "Geen logs vastgelegd."
|
"no_logs_captured": "Geen logs vastgelegd."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -983,6 +983,8 @@
|
||||||
"select_catalogs": "Wybierz katalogi",
|
"select_catalogs": "Wybierz katalogi",
|
||||||
"all_catalogs": "Wszystkie katalogi",
|
"all_catalogs": "Wszystkie katalogi",
|
||||||
"selected": "wybrane",
|
"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",
|
"hero_layout": "Układ sekcji Hero",
|
||||||
"layout_legacy": "Klasyczny",
|
"layout_legacy": "Klasyczny",
|
||||||
"layout_carousel": "Karuzela",
|
"layout_carousel": "Karuzela",
|
||||||
|
|
|
||||||
|
|
@ -625,8 +625,7 @@
|
||||||
"enter_custom_key": "Te rugăm să introduci și să salvezi cheia personalizată.",
|
"enter_custom_key": "Te rugăm să introduci și să salvezi cheia personalizată.",
|
||||||
"key_verified": "Cheia API a fost verificată și salvată cu succes."
|
"key_verified": "Cheia API a fost verificată și salvată cu succes."
|
||||||
},
|
},
|
||||||
{
|
"settings": {
|
||||||
"settings": {
|
|
||||||
"language": "Limbă",
|
"language": "Limbă",
|
||||||
"select_language": "Selectează limba",
|
"select_language": "Selectează limba",
|
||||||
"english": "Engleză",
|
"english": "Engleză",
|
||||||
|
|
@ -1029,8 +1028,7 @@
|
||||||
"no_upcoming_found": "Niciun episod viitor găsit",
|
"no_upcoming_found": "Niciun episod viitor găsit",
|
||||||
"add_series_desc": "Adaugă seriale în bibliotecă pentru a vedea episoadele lor viitoare aici"
|
"add_series_desc": "Adaugă seriale în bibliotecă pentru a vedea episoadele lor viitoare aici"
|
||||||
},
|
},
|
||||||
{
|
"mdblist": {
|
||||||
"mdblist": {
|
|
||||||
"title": "Surse de evaluare",
|
"title": "Surse de evaluare",
|
||||||
"status_disabled": "MDBList dezactivat",
|
"status_disabled": "MDBList dezactivat",
|
||||||
"status_active": "Cheie API activă",
|
"status_active": "Cheie API activă",
|
||||||
|
|
@ -1429,4 +1427,4 @@
|
||||||
"no_logs_captured": "Niciun log capturat."
|
"no_logs_captured": "Niciun log capturat."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -625,8 +625,7 @@
|
||||||
"enter_custom_key": "Ju lutem jepni dhe ruani çelësin tuaj personal.",
|
"enter_custom_key": "Ju lutem jepni dhe ruani çelësin tuaj personal.",
|
||||||
"key_verified": "Çelësi API u verifikua dhe u ruajt me sukses."
|
"key_verified": "Çelësi API u verifikua dhe u ruajt me sukses."
|
||||||
},
|
},
|
||||||
{
|
"settings": {
|
||||||
"settings": {
|
|
||||||
"language": "Gjuha",
|
"language": "Gjuha",
|
||||||
"select_language": "Zgjidh Gjuhën",
|
"select_language": "Zgjidh Gjuhën",
|
||||||
"english": "Anglisht",
|
"english": "Anglisht",
|
||||||
|
|
@ -973,8 +972,7 @@
|
||||||
"alert_disconnect_title": "Shkëput Torbox",
|
"alert_disconnect_title": "Shkëput Torbox",
|
||||||
"alert_disconnect_msg": "Jeni të sigurt? Kjo do të fshijë çelësin API të ruajtur."
|
"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",
|
"title": "Cilësimet e Ekranit Kryesor",
|
||||||
"changes_applied": "Ndryshimet u aplikuan",
|
"changes_applied": "Ndryshimet u aplikuan",
|
||||||
"display_options": "OPSIONET E SHFAQJES",
|
"display_options": "OPSIONET E SHFAQJES",
|
||||||
|
|
@ -1429,4 +1427,4 @@
|
||||||
"no_logs_captured": "Nuk u kap asnjë log."
|
"no_logs_captured": "Nuk u kap asnjë log."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
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 { mmkvStorage } from '../services/mmkvStorage';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
|
@ -11,7 +11,7 @@ import { useToast } from '../contexts/ToastContext';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__';
|
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 normalizeAuthErrorMessage = (input: string): string => {
|
||||||
const raw = (input || '').trim();
|
const raw = (input || '').trim();
|
||||||
|
|
@ -425,6 +425,16 @@ const AuthScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{mode === 'signin' && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => Linking.openURL('https://nuvioapp.space/account/reset-password')}
|
||||||
|
activeOpacity={0.75}
|
||||||
|
style={styles.forgotPasswordButton}
|
||||||
|
>
|
||||||
|
<Text style={[styles.forgotPasswordText, { color: currentTheme.colors.textMuted }]}>Forgot password?</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Confirm Password (signup only) */}
|
{/* Confirm Password (signup only) */}
|
||||||
{mode === 'signup' && (
|
{mode === 'signup' && (
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
|
|
@ -744,6 +754,15 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
forgotPasswordButton: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
marginTop: -6,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
forgotPasswordText: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default AuthScreen;
|
export default AuthScreen;
|
||||||
|
|
|
||||||
|
|
@ -344,9 +344,22 @@ const HomeScreenSettings: React.FC = () => {
|
||||||
colors={colors}
|
colors={colors}
|
||||||
renderControl={ChevronRight}
|
renderControl={ChevronRight}
|
||||||
onPress={() => navigation.navigate('HeroCatalogs')}
|
onPress={() => navigation.navigate('HeroCatalogs')}
|
||||||
isLast={true}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<SettingItem
|
||||||
|
title={t("home_screen.prefer_external_meta")}
|
||||||
|
description={t("home_screen.prefer_external_meta_desc")}
|
||||||
|
icon="cloud-download"
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
colors={colors}
|
||||||
|
renderControl={() => (
|
||||||
|
<CustomSwitch
|
||||||
|
value={settings.preferExternalMetaAddonDetail}
|
||||||
|
onValueChange={(value) => handleUpdateSetting('preferExternalMetaAddonDetail', value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
isLast={true}
|
||||||
|
/>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|
||||||
{settings.showHeroSection && (
|
{settings.showHeroSection && (
|
||||||
|
|
|
||||||
|
|
@ -825,7 +825,11 @@ class SupabaseSyncService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeUrl(url: string): string {
|
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 {
|
private toBigIntNumber(value: unknown): number {
|
||||||
|
|
@ -1063,14 +1067,37 @@ class SupabaseSyncService {
|
||||||
.map((url) => this.normalizeUrl(url))
|
.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 || []) {
|
for (const row of rows || []) {
|
||||||
if (!row.url) continue;
|
if (!row.url) continue;
|
||||||
const normalized = this.normalizeUrl(row.url);
|
const normalized = this.normalizeUrl(row.url);
|
||||||
if (installedUrls.has(normalized)) continue;
|
if (installedUrls.has(normalized)) continue;
|
||||||
|
|
||||||
try {
|
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);
|
await stremioService.installAddon(row.url);
|
||||||
installedUrls.add(normalized);
|
installedUrls.add(normalized);
|
||||||
|
if (manifest?.id) installedAddonIds.add(manifest.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('[SupabaseSyncService] Failed to install synced addon:', row.url, error);
|
logger.warn('[SupabaseSyncService] Failed to install synced addon:', row.url, error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Single source of truth for the app version displayed in Settings
|
// Single source of truth for the app version displayed in Settings
|
||||||
// Update this when bumping app version
|
// Update this when bumping app version
|
||||||
|
|
||||||
export const APP_VERSION = '1.3.7';
|
export const APP_VERSION = '1.4.0';
|
||||||
|
|
||||||
export function getDisplayedAppVersion(): string {
|
export function getDisplayedAppVersion(): string {
|
||||||
return APP_VERSION;
|
return APP_VERSION;
|
||||||
|
|
|
||||||