From 680a1b1ea6e6ee3fb1b412aa43095c3fb778a202 Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 16 Sep 2025 01:34:38 +0530 Subject: [PATCH] fixes --- .../home/ContinueWatchingSection.tsx | 16 +++++-- src/components/player/AndroidVideoPlayer.tsx | 19 ++++++-- src/hooks/useMetadata.ts | 14 ++++++ src/screens/PluginsScreen.tsx | 32 ++++++++----- src/screens/StreamsScreen.tsx | 46 +++++++++---------- src/services/SyncService.ts | 30 ++++++++++-- src/services/storageService.ts | 46 +++++++++++-------- 7 files changed, 139 insertions(+), 64 deletions(-) diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 8cd85b3..48bdfbc 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -12,7 +12,7 @@ import { } from 'react-native'; import { FlashList } from '@shopify/flash-list'; import Animated, { FadeIn } from 'react-native-reanimated'; -import { useNavigation } from '@react-navigation/native'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; import { StreamingContent, catalogService } from '../../services/catalogService'; @@ -214,6 +214,8 @@ const ContinueWatchingSection = React.forwardRef((props, re const progressPercent = (progress.currentTime / progress.duration) * 100; // Skip fully watched movies if (type === 'movie' && progressPercent >= 85) continue; + // Skip movies with no actual progress (ensure > 0%) + if (type === 'movie' && (!isFinite(progressPercent) || progressPercent <= 0)) continue; const contentKey = `${type}:${id}`; if (!contentGroups[contentKey]) contentGroups[contentKey] = { type, id, episodes: [] }; contentGroups[contentKey].episodes.push({ key, episodeId, progress, progressPercent }); @@ -447,7 +449,7 @@ const ContinueWatchingSection = React.forwardRef((props, re refreshTimerRef.current = setTimeout(() => { // Trigger a background refresh loadContinueWatching(true); - }, 2000); // Increased debounce time significantly to reduce churn + }, 800); // Shorter debounce for snappier UI without battery impact }; // Try to set up a custom event listener or use a timer as fallback @@ -484,6 +486,14 @@ const ContinueWatchingSection = React.forwardRef((props, re loadContinueWatching(); }, [loadContinueWatching]); + // Refresh on screen focus (lightweight, no polling) + useFocusEffect( + useCallback(() => { + loadContinueWatching(true); + return () => {}; + }, [loadContinueWatching]) + ); + // Expose the refresh function via the ref React.useImperativeHandle(ref, () => ({ refresh: async () => { @@ -619,7 +629,7 @@ const ContinueWatchingSection = React.forwardRef((props, re {(() => { - const isUpNext = item.progress === 0; + const isUpNext = item.type === 'series' && item.progress === 0; return ( { (error?.error?.localizedDescription && error.error.localizedDescription.includes('server is not correctly configured')); + // Expand audio decoder error detection to include DTS/TrueHD/Atmos families + const isHeavyCodecDecoderError = + (error?.error?.errorString && /(dts|true\s?hd|truehd|atmos)/i.test(String(error.error.errorString))) || + (error?.error?.errorException && /(dts|true\s?hd|truehd|atmos)/i.test(String(error.error.errorException))); + // Format error details for user display let errorMessage = 'An unknown error occurred'; if (error) { if (isDolbyCodecError) { errorMessage = 'Audio codec compatibility issue detected. The video contains Dolby Digital Plus audio which is not supported on this device. Please try selecting a different audio track or use an alternative video source.'; + } else if (isHeavyCodecDecoderError) { + errorMessage = 'Audio codec issue (DTS/TrueHD/Atmos). Switching to a stereo/standard audio track may help.'; } else if (isServerConfigError) { errorMessage = 'Stream server configuration issue. This may be a temporary problem with the video source.'; } else if (typeof error === 'string') { @@ -2627,7 +2634,8 @@ const AndroidVideoPlayer: React.FC = () => { ignoreSilentSwitch="ignore" mixWithOthers="inherit" progressUpdateInterval={1000} - maxBitRate={2000000} + // Remove artificial bit rate cap to allow high-bitrate streams (e.g., Blu-ray remux) to play + // maxBitRate intentionally omitted disableFocus={true} // iOS AVPlayer startup tuning automaticallyWaitsToMinimizeStalling={true as any} @@ -2636,11 +2644,14 @@ const AndroidVideoPlayer: React.FC = () => { preventsDisplaySleepDuringVideoPlayback={true as any} // ExoPlayer HLS optimization bufferConfig={{ - minBufferMs: 15000, - maxBufferMs: 50000, + // Larger buffers for high-bitrate remuxes to reduce rebuffering/crashes + minBufferMs: 60000, + maxBufferMs: 180000, bufferForPlaybackMs: 2500, - bufferForPlaybackAfterRebufferMs: 5000, + bufferForPlaybackAfterRebufferMs: 8000, } as any} + // Use SurfaceView on Android to lower memory pressure with 4K/high-bitrate content + useTextureView={Platform.OS === 'android' ? false : (undefined as any)} /> diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 2103a57..ab2d274 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -88,6 +88,7 @@ interface UseMetadataReturn { loadingStreams: boolean; episodeStreams: GroupedStreams; loadingEpisodeStreams: boolean; + addonResponseOrder: string[]; preloadedStreams: GroupedStreams; preloadedEpisodeStreams: { [episodeId: string]: GroupedStreams }; selectedEpisode: string | null; @@ -134,6 +135,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const [availableStreams, setAvailableStreams] = useState<{ [sourceType: string]: Stream }>({}); const [scraperStatuses, setScraperStatuses] = useState([]); const [activeFetchingScrapers, setActiveFetchingScrapers] = useState([]); + // Track response order for addons to preserve actual response order + const [addonResponseOrder, setAddonResponseOrder] = useState([]); // Prevent re-initializing season selection repeatedly for the same series const initializedSeasonRef = useRef(false); @@ -287,6 +290,14 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat }; }; + // Track response order for addons + setAddonResponseOrder(prevOrder => { + if (!prevOrder.includes(addonId)) { + return [...prevOrder, addonId]; + } + return prevOrder; + }); + if (isEpisode) { setEpisodeStreams(updateState); setLoadingEpisodeStreams(false); @@ -824,6 +835,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Reset scraper tracking setScraperStatuses([]); setActiveFetchingScrapers([]); + setAddonResponseOrder([]); // Reset response order // Get TMDB ID for external sources and determine the correct ID for Stremio addons if (__DEV__) console.log('🔍 [loadStreams] Getting TMDB ID for:', id); @@ -990,6 +1002,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Reset scraper tracking for episodes setScraperStatuses([]); setActiveFetchingScrapers([]); + setAddonResponseOrder([]); // Reset response order // Initialize scraper tracking for episodes try { @@ -1358,6 +1371,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat loadingStreams, episodeStreams, loadingEpisodeStreams, + addonResponseOrder, preloadedStreams, preloadedEpisodeStreams, selectedEpisode, diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx index f786386..0a27b1d 100644 --- a/src/screens/PluginsScreen.tsx +++ b/src/screens/PluginsScreen.tsx @@ -705,26 +705,36 @@ const StatusBadge: React.FC<{ const getStatusConfig = () => { switch (status) { case 'enabled': - return { color: '#34C759', text: 'Active', icon: 'checkmark-circle' }; + return { color: '#34C759', text: 'Active' }; case 'disabled': - return { color: colors.mediumGray, text: 'Disabled', icon: 'close-circle' }; + return { color: colors.mediumGray, text: 'Disabled' }; case 'available': - return { color: colors.primary, text: 'Available', icon: 'download' }; + return { color: colors.primary, text: 'Available' }; case 'platform-disabled': - return { color: '#FF9500', text: 'Platform Disabled', icon: 'phone-portrait' }; + return { color: '#FF9500', text: 'Platform Disabled' }; case 'error': - return { color: '#FF3B30', text: 'Error', icon: 'warning' }; + return { color: '#FF3B30', text: 'Error' }; default: - return { color: colors.mediumGray, text: 'Unknown', icon: 'help-circle' }; + return { color: colors.mediumGray, text: 'Unknown' }; } }; const config = getStatusConfig(); return ( - - - {config.text} + + + {config.text} ); }; @@ -874,7 +884,7 @@ const PluginsScreen: React.FC = () => { setShowAddRepositoryModal(false); Alert.alert('Success', 'Repository added and refreshed successfully'); } catch (error) { - logger.error('[ScraperSettings] Failed to add repository:', error); + logger.error('[PluginsScreen] Failed to add repository:', error); Alert.alert('Error', 'Failed to add repository'); } finally { setIsLoading(false); @@ -1447,7 +1457,7 @@ const PluginsScreen: React.FC = () => { )} - + {scraper.name} diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 70eb34a..2fd7303 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -455,6 +455,7 @@ export const StreamsScreen = () => { imdbId, scraperStatuses, activeFetchingScrapers, + addonResponseOrder, } = useMetadata({ id, type }); // Get backdrop from metadata assets @@ -1378,13 +1379,20 @@ export const StreamsScreen = () => { return addonId === selectedProvider; }) .sort(([addonIdA], [addonIdB]) => { - // Sort by Stremio addon installation order - const indexA = installedAddons.findIndex(addon => addon.id === addonIdA); - const indexB = installedAddons.findIndex(addon => addon.id === addonIdB); - - if (indexA !== -1 && indexB !== -1) return indexA - indexB; + // Sort by response order (actual order addons responded) + const indexA = addonResponseOrder.indexOf(addonIdA); + const indexB = addonResponseOrder.indexOf(addonIdB); + + // If both are in response order, sort by response order + if (indexA !== -1 && indexB !== -1) { + return indexA - indexB; + } + + // If only one is in response order, prioritize it if (indexA !== -1) return -1; if (indexB !== -1) return 1; + + // If neither is in response order, maintain original order return 0; }); @@ -1408,17 +1416,16 @@ export const StreamsScreen = () => { pluginOriginalCount += providerStreams.length; } - // Apply quality filtering and sorting to streams + // Apply quality filtering only; keep original addon stream order const filteredStreams = filterStreamsByQuality(providerStreams); - const sortedStreams = sortStreams(filteredStreams); if (isInstalledAddon) { - addonStreams.push(...sortedStreams); + addonStreams.push(...filteredStreams); if (!addonNames.includes(addonName)) { addonNames.push(addonName); } } else { - pluginStreams.push(...sortedStreams); + pluginStreams.push(...filteredStreams); if (!pluginNames.includes(addonName)) { pluginNames.push(addonName); } @@ -1427,14 +1434,10 @@ export const StreamsScreen = () => { const sections = []; if (addonStreams.length > 0) { - // Apply final sorting to the combined addon streams for quality-first mode - const finalSortedAddonStreams = settings.streamSortMode === 'quality-then-scraper' ? - sortStreams(addonStreams) : addonStreams; - sections.push({ title: addonNames.join(', '), addonId: 'grouped-addons', - data: finalSortedAddonStreams + data: addonStreams }); } else if (addonOriginalCount > 0 && addonStreams.length === 0) { // Show empty section with message for addons that had streams but all were filtered @@ -1447,14 +1450,10 @@ export const StreamsScreen = () => { } if (pluginStreams.length > 0) { - // Apply final sorting to the combined plugin streams for quality-first mode - const finalSortedPluginStreams = settings.streamSortMode === 'quality-then-scraper' ? - sortStreams(pluginStreams) : pluginStreams; - sections.push({ title: localScraperService.getRepositoryName(), addonId: 'grouped-plugins', - data: finalSortedPluginStreams + data: pluginStreams }); } else if (pluginOriginalCount > 0 && pluginStreams.length === 0) { // Show empty section with message for plugins that had streams but all were filtered @@ -1473,21 +1472,20 @@ export const StreamsScreen = () => { // Count original streams before filtering const originalCount = providerStreams.length; - // Apply quality filtering and sorting to streams + // Apply quality filtering only; keep original addon stream order const filteredStreams = filterStreamsByQuality(providerStreams); - const sortedStreams = sortStreams(filteredStreams); - const isEmptyDueToQualityFilter = originalCount > 0 && sortedStreams.length === 0; + const isEmptyDueToQualityFilter = originalCount > 0 && filteredStreams.length === 0; return { title: addonName, addonId, - data: isEmptyDueToQualityFilter ? [{ isEmptyPlaceholder: true } as any] : sortedStreams, + data: isEmptyDueToQualityFilter ? [{ isEmptyPlaceholder: true } as any] : filteredStreams, isEmptyDueToQualityFilter }; }); } - }, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality, sortStreams]); + }, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality, sortStreams, addonResponseOrder]); const episodeImage = useMemo(() => { if (episodeThumbnail) { diff --git a/src/services/SyncService.ts b/src/services/SyncService.ts index 3f23927..4d4b6dc 100644 --- a/src/services/SyncService.ts +++ b/src/services/SyncService.ts @@ -113,19 +113,28 @@ class SyncService { await storageService.addWatchProgressTombstone(id, type, episodeId || undefined, remoteUpdated); } catch {} } else { + // Preserve the most recent timestamp between local and remote to maintain proper continue watching order + const remoteTimestamp = row.last_updated_ms || Date.now(); + const existingProgress = await storageService.getWatchProgress(id, type, (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined); + const localTimestamp = existingProgress?.lastUpdated || 0; + + // Use the newer timestamp to maintain proper continue watching order across devices + const finalTimestamp = Math.max(remoteTimestamp, localTimestamp); + await storageService.setWatchProgress( id, type, { currentTime: row.current_time_seconds || 0, duration: row.duration_seconds || 0, - lastUpdated: row.last_updated_ms || Date.now(), + lastUpdated: finalTimestamp, traktSynced: row.trakt_synced ?? undefined, traktLastSynced: row.trakt_last_synced_ms ?? undefined, traktProgress: row.trakt_progress_percent ?? undefined, }, // Ensure we pass through the full remote episode_id as-is; empty string becomes undefined - (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined + (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined, + { preserveTimestamp: true, forceNotify: true, forceWrite: true } ); } } catch {} @@ -372,19 +381,32 @@ class SyncService { if (wp && Array.isArray(wp)) { const remoteActiveKeys = new Set(); for (const row of wp as any[]) { + // Preserve the most recent timestamp between local and remote to maintain proper continue watching order + const remoteTimestamp = row.last_updated_ms || Date.now(); + const existingProgress = await storageService.getWatchProgress( + row.media_id, + row.media_type, + (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined + ); + const localTimestamp = existingProgress?.lastUpdated || 0; + + // Use the newer timestamp to maintain proper continue watching order across devices + const finalTimestamp = Math.max(remoteTimestamp, localTimestamp); + await storageService.setWatchProgress( row.media_id, row.media_type, { currentTime: row.current_time_seconds, duration: row.duration_seconds, - lastUpdated: row.last_updated_ms, + lastUpdated: finalTimestamp, traktSynced: row.trakt_synced ?? undefined, traktLastSynced: row.trakt_last_synced_ms ?? undefined, traktProgress: row.trakt_progress_percent ?? undefined, }, // Ensure full episode_id is preserved; treat empty as undefined - (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined + (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined, + { preserveTimestamp: true, forceNotify: true, forceWrite: true } ); remoteActiveKeys.add(`${row.media_type}|${row.media_id}|${row.episode_id || ''}`); } diff --git a/src/services/storageService.ts b/src/services/storageService.ts index b1b8e73..4d09480 100644 --- a/src/services/storageService.ts +++ b/src/services/storageService.ts @@ -223,10 +223,11 @@ class StorageService { } public async setWatchProgress( - id: string, - type: string, + id: string, + type: string, progress: WatchProgress, - episodeId?: string + episodeId?: string, + options?: { preserveTimestamp?: boolean; forceNotify?: boolean; forceWrite?: boolean } ): Promise { try { const key = await this.getWatchProgressKeyScoped(id, type, episodeId); @@ -243,23 +244,32 @@ class StorageService { } } catch {} - // Check if progress has actually changed significantly - const existingProgress = await this.getWatchProgress(id, type, episodeId); - if (existingProgress) { - const timeDiff = Math.abs(progress.currentTime - existingProgress.currentTime); - const durationDiff = Math.abs(progress.duration - existingProgress.duration); - - // Only update if there's a significant change (>5 seconds or duration change) - if (timeDiff < 5 && durationDiff < 1) { - return; // Skip update for minor changes + // Check if progress has actually changed significantly, unless forceWrite is requested + if (!options?.forceWrite) { + const existingProgress = await this.getWatchProgress(id, type, episodeId); + if (existingProgress) { + const timeDiff = Math.abs(progress.currentTime - existingProgress.currentTime); + const durationDiff = Math.abs(progress.duration - existingProgress.duration); + + // Only update if there's a significant change (>5 seconds or duration change) + if (timeDiff < 5 && durationDiff < 1) { + return; // Skip update for minor changes + } } } - - const updated = { ...progress, lastUpdated: Date.now() }; - await AsyncStorage.setItem(key, JSON.stringify(updated)); - - // Use debounced notification to reduce spam - this.debouncedNotifySubscribers(); + + const timestamp = (options?.preserveTimestamp && typeof progress.lastUpdated === 'number') + ? progress.lastUpdated + : Date.now(); + const updated = { ...progress, lastUpdated: timestamp }; + await AsyncStorage.setItem(key, JSON.stringify(updated)); + + // Notify subscribers; allow forcing immediate notification + if (options?.forceNotify) { + this.notifyWatchProgressSubscribers(); + } else { + this.debouncedNotifySubscribers(); + } } catch (error) { logger.error('Error setting watch progress:', error); }