Merge branch 'tapframe:main' into DNS

This commit is contained in:
paregi12 2026-02-20 13:07:07 +05:30 committed by GitHub
commit 49e6c1f500
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 190 additions and 42 deletions

View file

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

View file

@ -120,6 +120,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 = {
@ -212,6 +214,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';

View file

@ -1007,6 +1007,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",

View file

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

View file

@ -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';
@ -425,6 +425,16 @@ const AuthScreen: React.FC = () => {
</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) */}
{mode === 'signup' && (
<View style={styles.inputContainer}>
@ -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;

View file

@ -344,9 +344,22 @@ const HomeScreenSettings: React.FC = () => {
colors={colors}
renderControl={ChevronRight}
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>
{settings.showHeroSection && (

View file

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