From 2dd2b7fc0b53566260eadcab8ccdcd4975502c4e Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 17 Sep 2025 03:49:22 +0530 Subject: [PATCH] TEST --- src/components/player/AndroidVideoPlayer.tsx | 397 +++++++++++------- .../player/controls/PlayerControls.tsx | 12 +- .../player/modals/AudioTrackModal.tsx | 77 ++-- src/services/SyncService.ts | 31 +- src/services/stremioService.ts | 100 ++++- 5 files changed, 404 insertions(+), 213 deletions(-) diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index aa868fde..1e609370 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -189,7 +189,7 @@ const AndroidVideoPlayer: React.FC = () => { const [duration, setDuration] = useState(0); const [showControls, setShowControls] = useState(true); const [audioTracks, setAudioTracks] = useState([]); - const [selectedAudioTrack, setSelectedAudioTrack] = useState(null); + const [selectedAudioTrack, setSelectedAudioTrack] = useState({ type: SelectedTrackType.SYSTEM }); const [textTracks, setTextTracks] = useState([]); const [selectedTextTrack, setSelectedTextTrack] = useState(-1); const [resizeMode, setResizeMode] = useState('contain'); @@ -280,6 +280,13 @@ const AndroidVideoPlayer: React.FC = () => { const [errorDetails, setErrorDetails] = useState(''); const errorTimeoutRef = useRef(null); + // Toast state for codec unsupported notifications + const [showCodecToast, setShowCodecToast] = useState(false); + const [codecToastMessage, setCodecToastMessage] = useState(''); + const codecToastOpacity = useRef(new Animated.Value(0)).current; + const codecToastTranslateY = useRef(new Animated.Value(-20)).current; + const codecToastTimeout = useRef(null); + // Volume and brightness controls const [volume, setVolume] = useState(1.0); const [brightness, setBrightness] = useState(1.0); @@ -794,8 +801,8 @@ const AndroidVideoPlayer: React.FC = () => { clearInterval(progressSaveInterval); } - // HEATING FIX: Increase sync interval to 15 seconds to reduce CPU load - const syncInterval = 15000; // 15 seconds to prevent heating + // Sync interval for progress updates + const syncInterval = 5000; // 5 seconds for responsive progress tracking const interval = setInterval(() => { saveWatchProgress(); @@ -969,33 +976,13 @@ const AndroidVideoPlayer: React.FC = () => { if (now - lastAudioTrackCheck > 3000 && !paused && duration > 0 && audioTrackFallbackAttempts < 3) { setLastAudioTrackCheck(now); - // Check if audio track is disabled (-1) and we have available tracks - if (selectedAudioTrack === -1 && rnVideoAudioTracks.length > 1) { + // Check if audio track is disabled and we have available tracks + if (selectedAudioTrack?.type === SelectedTrackType.DISABLED && rnVideoAudioTracks.length > 1) { logger.warn('[AndroidVideoPlayer] Detected disabled audio track, attempting fallback'); - // Find a fallback audio track (prioritize AAC/stereo over heavy codecs) + // Find any available audio track const fallbackTrack = rnVideoAudioTracks.find((track) => { - const trackName = (track.name || '').toLowerCase(); - // Prefer AAC, stereo, or standard audio formats, avoid heavy codecs - const isCompatible = !trackName.includes('truehd') && - !trackName.includes('dts') && - !trackName.includes('atmos') && - track.id !== selectedAudioTrack; // Don't select the same track - - // Prioritize AAC and stereo tracks - const isPreferred = trackName.includes('aac') || - trackName.includes('stereo') || - trackName.includes('2.0') || - trackName.includes('2ch'); - - return isCompatible && isPreferred; - }) || rnVideoAudioTracks.find((track) => { - // Fallback: any compatible track (even if not preferred) - const trackName = (track.name || '').toLowerCase(); - return !trackName.includes('truehd') && - !trackName.includes('dts') && - !trackName.includes('atmos') && - track.id !== selectedAudioTrack; + return track.id !== (selectedAudioTrack?.type === SelectedTrackType.INDEX ? selectedAudioTrack.value : null); }); if (fallbackTrack) { @@ -1004,8 +991,8 @@ const AndroidVideoPlayer: React.FC = () => { // Increment fallback attempts counter setAudioTrackFallbackAttempts(prev => prev + 1); - // Switch to fallback audio track - setSelectedAudioTrack(fallbackTrack.id); + // Switch to manual track selection + setSelectedAudioTrack({ type: SelectedTrackType.INDEX, value: fallbackTrack.id }); // Brief pause to allow track switching setPaused(true); @@ -1199,41 +1186,13 @@ const AndroidVideoPlayer: React.FC = () => { }); setRnVideoAudioTracks(formattedAudioTracks); - // Auto-select compatible audio track (prioritize AAC/stereo over heavy codecs) - if (selectedAudioTrack === null && formattedAudioTracks.length > 0) { - // First, try to find a compatible English track - const compatibleEnglishTrack = formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => { - const lang = (track.language || '').toLowerCase(); - const trackName = (track.name || '').toLowerCase(); - const isEnglish = lang === 'english' || lang === 'en' || lang === 'eng' || - (track.name && track.name.toLowerCase().includes('english')); - const isCompatible = !trackName.includes('truehd') && - !trackName.includes('dts') && - !trackName.includes('atmos'); - return isEnglish && isCompatible; - }); - - // If no compatible English track, find any compatible track - const compatibleTrack = compatibleEnglishTrack || formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => { - const trackName = (track.name || '').toLowerCase(); - return !trackName.includes('truehd') && - !trackName.includes('dts') && - !trackName.includes('atmos'); - }); - - // Fallback to any track if no compatible ones found - const selectedTrack = compatibleTrack || formattedAudioTracks[0]; - setSelectedAudioTrack(selectedTrack.id); - + // Use system auto-selection + if (selectedAudioTrack?.type === SelectedTrackType.SYSTEM && formattedAudioTracks.length > 0) { if (DEBUG_MODE) { - if (compatibleEnglishTrack) { - logger.log(`[AndroidVideoPlayer] Auto-selected compatible English audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`); - } else if (compatibleTrack) { - logger.log(`[AndroidVideoPlayer] Auto-selected compatible audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`); - } else { - logger.log(`[AndroidVideoPlayer] No compatible tracks found, auto-selected first audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`); - } + logger.log(`[AndroidVideoPlayer] Using system auto-selection for ${formattedAudioTracks.length} audio tracks`); + logger.log(`[AndroidVideoPlayer] Available tracks:`, formattedAudioTracks.map((t: any) => `${t.name} (${t.language})`)); } + // Keep using system selection } if (DEBUG_MODE) { @@ -1474,68 +1433,92 @@ const AndroidVideoPlayer: React.FC = () => { if (isAudioCodecError && rnVideoAudioTracks.length > 0) { logger.warn('[AndroidVideoPlayer] Audio codec error detected, attempting audio track fallback'); - // Find a fallback audio track (prioritize AAC/stereo over heavy codecs) - const fallbackTrack = rnVideoAudioTracks.find((track) => { - const trackName = (track.name || '').toLowerCase(); - // Prefer AAC, stereo, or standard audio formats, avoid heavy codecs - const isCompatible = !trackName.includes('truehd') && - !trackName.includes('dts') && - !trackName.includes('atmos') && - track.id !== selectedAudioTrack; // Don't select the same track - - // Prioritize AAC and stereo tracks - const isPreferred = trackName.includes('aac') || - trackName.includes('stereo') || - trackName.includes('2.0') || - trackName.includes('2ch'); - - return isCompatible && isPreferred; - }) || rnVideoAudioTracks.find((track) => { - // Fallback: any compatible track (even if not preferred) - const trackName = (track.name || '').toLowerCase(); - return !trackName.includes('truehd') && - !trackName.includes('dts') && - !trackName.includes('atmos') && - track.id !== selectedAudioTrack; - }); + // Show toast notification about codec issue + showCodecUnsupportedToast('Audio codec not supported on this device. Switching to compatible track...'); - if (fallbackTrack) { - logger.warn(`[AndroidVideoPlayer] Switching to fallback audio track: ${fallbackTrack.name || 'Unknown'} (id: ${fallbackTrack.id})`); + // If using system selection failed, try manual selection + if (selectedAudioTrack?.type === SelectedTrackType.SYSTEM) { + logger.log('[AndroidVideoPlayer] System audio selection failed, falling back to manual selection'); - // Clear any existing error state - if (errorTimeoutRef.current) { - clearTimeout(errorTimeoutRef.current); - errorTimeoutRef.current = null; + // Find any available audio track + const fallbackTrack = rnVideoAudioTracks[0]; + + if (fallbackTrack) { + logger.warn(`[AndroidVideoPlayer] Switching to fallback audio track: ${fallbackTrack.name || 'Unknown'} (id: ${fallbackTrack.id})`); + + // Clear any existing error state + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + errorTimeoutRef.current = null; + } + safeSetState(() => setShowErrorModal(false)); + + // Switch to manual track selection + setSelectedAudioTrack({ type: SelectedTrackType.INDEX, value: fallbackTrack.id }); + + // Brief pause to allow track switching + setPaused(true); + setTimeout(() => { + if (!isMounted.current) return; + setPaused(false); + }, 500); + + return; // Don't show error UI, attempt recovery } - safeSetState(() => setShowErrorModal(false)); - - // Switch to fallback audio track - setSelectedAudioTrack(fallbackTrack.id); - - // Brief pause to allow track switching - setPaused(true); - setTimeout(() => { - if (!isMounted.current) return; - setPaused(false); - }, 500); - - return; // Don't show error UI, attempt recovery - } else { - // As a last resort, disable audio entirely to keep video playing - logger.warn('[AndroidVideoPlayer] No compatible audio track found. Disabling audio to keep playback.'); - if (errorTimeoutRef.current) { - clearTimeout(errorTimeoutRef.current); - errorTimeoutRef.current = null; - } - safeSetState(() => setShowErrorModal(false)); - setSelectedAudioTrack(-1); // handled as DISABLED by selectedTextTrack prop - setPaused(true); - setTimeout(() => { - if (!isMounted.current) return; - setPaused(false); - }, 300); - return; } + + // For manual selections that failed, try a different manual track + const currentTrackId = selectedAudioTrack?.type === SelectedTrackType.INDEX ? selectedAudioTrack.value : null; + if (currentTrackId !== null) { + const fallbackTrack = rnVideoAudioTracks.find((track) => { + return track.id !== currentTrackId; + }); + + if (fallbackTrack) { + logger.warn(`[AndroidVideoPlayer] Switching from failed track ${currentTrackId} to: ${fallbackTrack.name || 'Unknown'} (id: ${fallbackTrack.id})`); + + // Show toast notification about track switching + showCodecUnsupportedToast('Audio codec not supported. Trying alternative track...'); + + // Clear any existing error state + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + errorTimeoutRef.current = null; + } + safeSetState(() => setShowErrorModal(false)); + + // Switch to different manual track + setSelectedAudioTrack({ type: SelectedTrackType.INDEX, value: fallbackTrack.id }); + + // Brief pause to allow track switching + setPaused(true); + setTimeout(() => { + if (!isMounted.current) return; + setPaused(false); + }, 500); + + return; // Don't show error UI, attempt recovery + } + } + + // As a last resort, disable audio entirely to keep video playing + logger.warn('[AndroidVideoPlayer] No compatible audio track found. Disabling audio to keep playback.'); + + // Show toast notification about audio being disabled + showCodecUnsupportedToast('No compatible audio tracks found. Audio disabled to continue playback.'); + + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + errorTimeoutRef.current = null; + } + safeSetState(() => setShowErrorModal(false)); + setSelectedAudioTrack({ type: SelectedTrackType.DISABLED }); + setPaused(true); + setTimeout(() => { + if (!isMounted.current) return; + setPaused(false); + }, 300); + return; } // Detect Xprime provider to enable a one-shot silent retry (warms upstream/cache) @@ -1649,7 +1632,7 @@ const AndroidVideoPlayer: React.FC = () => { (error?.error?.localizedDescription && error.error.localizedDescription.includes('server is not correctly configured')); - // Expand audio decoder error detection to include DTS/TrueHD/Atmos families + // Audio decoder error detection 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))); @@ -1658,9 +1641,9 @@ const AndroidVideoPlayer: React.FC = () => { let errorMessage = 'An unknown error occurred'; if (error) { if (isAudioCodecError) { - errorMessage = 'Audio codec compatibility issue detected. The video contains unsupported audio codec (TrueHD/DTS/Dolby). Please try selecting a different audio track or use an alternative video source.'; + errorMessage = 'Audio compatibility issue detected. 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.'; + errorMessage = 'Audio decoder issue detected. Please try selecting a different audio track.'; } else if (isServerConfigError) { errorMessage = 'Stream server configuration issue. This may be a temporary problem with the video source.'; } else if (typeof error === 'string') { @@ -1682,7 +1665,7 @@ const AndroidVideoPlayer: React.FC = () => { } } - // For audio codec errors we already attempted recovery; avoid showing the modal + // For audio errors we already attempted recovery; avoid showing the modal if (!isAudioCodecError) { // Use safeSetState to prevent crashes on iOS when component is unmounted safeSetState(() => { @@ -1783,17 +1766,32 @@ const AndroidVideoPlayer: React.FC = () => { } }; - const selectAudioTrack = (trackId: number) => { + const selectAudioTrack = (trackSelection: SelectedTrack) => { if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Selecting audio track: ${trackId}`); + logger.log(`[AndroidVideoPlayer] Selecting audio track:`, trackSelection); logger.log(`[AndroidVideoPlayer] Available tracks:`, rnVideoAudioTracks); } - // Validate that the track exists - const trackExists = rnVideoAudioTracks.some(track => track.id === trackId); - if (!trackExists) { - logger.error(`[AndroidVideoPlayer] Audio track ${trackId} not found in available tracks`); - return; + // Validate track selection + if (trackSelection.type === SelectedTrackType.INDEX) { + const trackExists = rnVideoAudioTracks.some(track => track.id === trackSelection.value); + if (!trackExists) { + logger.error(`[AndroidVideoPlayer] Audio track ${trackSelection.value} not found in available tracks`); + return; + } + + // Check if the selected track might have codec compatibility issues + const selectedTrack = rnVideoAudioTracks.find(track => track.id === trackSelection.value); + if (selectedTrack) { + const trackName = (selectedTrack.name || '').toLowerCase(); + const hasHeavyCodec = trackName.includes('truehd') || trackName.includes('dts') || trackName.includes('atmos') || + trackName.includes('eac3') || trackName.includes('dolby') || trackName.includes('hdma'); + + if (hasHeavyCodec) { + // Show toast warning about potential codec issues + showCodecUnsupportedToast(`Audio codec may not be supported on this device. Try selecting a different track if playback fails.`); + } + } } // If changing tracks, briefly pause to allow smooth transition @@ -1803,10 +1801,10 @@ const AndroidVideoPlayer: React.FC = () => { } // Set the new audio track - setSelectedAudioTrack(trackId); + setSelectedAudioTrack(trackSelection); if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Audio track changed to: ${trackId}`); + logger.log(`[AndroidVideoPlayer] Audio track changed to:`, trackSelection); } // Resume playback after a brief delay if it was playing @@ -2347,6 +2345,9 @@ const AndroidVideoPlayer: React.FC = () => { if (brightnessOverlayTimeout.current) { clearTimeout(brightnessOverlayTimeout.current); } + if (codecToastTimeout.current) { + clearTimeout(codecToastTimeout.current); + } }; }, []); @@ -2356,6 +2357,55 @@ const AndroidVideoPlayer: React.FC = () => { } }; + // Function to show codec unsupported toast + const showCodecUnsupportedToast = (message: string) => { + if (!isMounted.current) return; + + // Clear any existing timeout + if (codecToastTimeout.current) { + clearTimeout(codecToastTimeout.current); + } + + setCodecToastMessage(message); + setShowCodecToast(true); + + // Animate toast in + Animated.parallel([ + Animated.timing(codecToastOpacity, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(codecToastTranslateY, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }) + ]).start(); + + // Auto-hide after 3 seconds + codecToastTimeout.current = setTimeout(() => { + if (isMounted.current) { + Animated.parallel([ + Animated.timing(codecToastOpacity, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }), + Animated.timing(codecToastTranslateY, { + toValue: -20, + duration: 250, + useNativeDriver: true, + }) + ]).start(() => { + if (isMounted.current) { + setShowCodecToast(false); + } + }); + } + }, 3000); + }; + useEffect(() => { if (!useCustomSubtitles || customSubtitles.length === 0) { if (currentSubtitle !== '') { @@ -2378,13 +2428,23 @@ const AndroidVideoPlayer: React.FC = () => { // Handle audio track changes with proper logging useEffect(() => { if (selectedAudioTrack !== null && rnVideoAudioTracks.length > 0) { - const selectedTrack = rnVideoAudioTracks.find(track => track.id === selectedAudioTrack); - if (selectedTrack) { - if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Audio track selected: ${selectedTrack.name} (${selectedTrack.language}) - ID: ${selectedAudioTrack}`); + if (selectedAudioTrack.type === SelectedTrackType.INDEX && selectedAudioTrack.value !== undefined) { + const selectedTrack = rnVideoAudioTracks.find(track => track.id === selectedAudioTrack.value); + if (selectedTrack) { + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Audio track selected: ${selectedTrack.name} (${selectedTrack.language}) - ID: ${selectedAudioTrack.value}`); + } + } else { + logger.warn(`[AndroidVideoPlayer] Selected audio track ${selectedAudioTrack.value} not found in available tracks`); + } + } else if (selectedAudioTrack.type === SelectedTrackType.SYSTEM) { + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Using system audio selection`); + } + } else if (selectedAudioTrack.type === SelectedTrackType.DISABLED) { + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Audio disabled`); } - } else { - logger.warn(`[AndroidVideoPlayer] Selected audio track ${selectedAudioTrack} not found in available tracks`); } } }, [selectedAudioTrack, rnVideoAudioTracks]); @@ -2872,7 +2932,7 @@ const AndroidVideoPlayer: React.FC = () => { onBuffer(buf); }} resizeMode={getVideoResizeMode(resizeMode)} - selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined} + selectedAudioTrack={selectedAudioTrack || undefined} selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)} rate={1.0} volume={volume} @@ -2882,23 +2942,14 @@ const AndroidVideoPlayer: React.FC = () => { playWhenInactive={false} ignoreSilentSwitch="ignore" mixWithOthers="inherit" - progressUpdateInterval={1000} + progressUpdateInterval={250} // 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} - preferredForwardBufferDuration={1 as any} + // iOS AVPlayer optimization allowsExternalPlayback={false as any} preventsDisplaySleepDuringVideoPlayback={true as any} - // ExoPlayer HLS optimization - bufferConfig={{ - // Larger buffers for high-bitrate remuxes to reduce rebuffering/crashes - minBufferMs: 60000, - maxBufferMs: 180000, - bufferForPlaybackMs: 2500, - bufferForPlaybackAfterRebufferMs: 8000, - } as any} + // ExoPlayer HLS optimization - let the player use optimal defaults // Use SurfaceView on Android to lower memory pressure with 4K/high-bitrate content useTextureView={Platform.OS === 'android' ? false : (undefined as any)} /> @@ -3524,6 +3575,48 @@ const AndroidVideoPlayer: React.FC = () => { + {/* Codec Unsupported Toast */} + {showCodecToast && ( + + + + + {codecToastMessage} + + + + )} + ; - selectedAudioTrack: number | null; + selectedAudioTrack: SelectedTrack | null; availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; togglePlayback: () => void; skip: (seconds: number) => void; @@ -177,8 +178,13 @@ export const PlayerControls: React.FC = ({ disabled={vlcAudioTracks.length <= 1} > - - {`Audio: ${getTrackDisplayName(vlcAudioTracks.find(t => t.id === selectedAudioTrack) || {id: -1, name: 'Default'})}`} + + {(() => { + const trackName = getTrackDisplayName(vlcAudioTracks.find(t => t.id === (selectedAudioTrack?.type === SelectedTrackType.INDEX ? selectedAudioTrack.value : null)) || {id: -1, name: 'Default'}); + // Truncate long audio track names to prevent UI cramping + const maxLength = 12; // Limit to 12 characters + return trackName.length > maxLength ? `${trackName.substring(0, maxLength)}...` : trackName; + })()} diff --git a/src/components/player/modals/AudioTrackModal.tsx b/src/components/player/modals/AudioTrackModal.tsx index c46f6574..f9dff247 100644 --- a/src/components/player/modals/AudioTrackModal.tsx +++ b/src/components/player/modals/AudioTrackModal.tsx @@ -9,13 +9,14 @@ import Animated, { } from 'react-native-reanimated'; import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils'; import { logger } from '../../../utils/logger'; +import { SelectedTrack, SelectedTrackType } from 'react-native-video'; interface AudioTrackModalProps { showAudioModal: boolean; setShowAudioModal: (show: boolean) => void; vlcAudioTracks: Array<{id: number, name: string, language?: string}>; - selectedAudioTrack: number | null; - selectAudioTrack: (trackId: number) => void; + selectedAudioTrack: SelectedTrack | null; + selectAudioTrack: (trackSelection: SelectedTrack) => void; } const { width } = Dimensions.get('window'); @@ -35,13 +36,17 @@ export const AudioTrackModal: React.FC = ({ // Debug logging when modal opens React.useEffect(() => { if (showAudioModal && DEBUG_MODE) { - logger.log(`[AudioTrackModal] Modal opened with selectedAudioTrack: ${selectedAudioTrack}`); + logger.log(`[AudioTrackModal] Modal opened with selectedAudioTrack:`, selectedAudioTrack); logger.log(`[AudioTrackModal] Available tracks:`, vlcAudioTracks); - const selectedTrack = vlcAudioTracks.find(track => track.id === selectedAudioTrack); - if (selectedTrack) { - logger.log(`[AudioTrackModal] Selected track found: ${selectedTrack.name} (${selectedTrack.language})`); - } else { - logger.warn(`[AudioTrackModal] Selected track ${selectedAudioTrack} not found in available tracks`); + if (selectedAudioTrack?.type === 'index' && selectedAudioTrack.value !== undefined) { + const selectedTrack = vlcAudioTracks.find(track => track.id === selectedAudioTrack.value); + if (selectedTrack) { + logger.log(`[AudioTrackModal] Selected track found: ${selectedTrack.name} (${selectedTrack.language})`); + } else { + logger.warn(`[AudioTrackModal] Selected track ${selectedAudioTrack.value} not found in available tracks`); + } + } else if (selectedAudioTrack?.type === 'system') { + logger.log(`[AudioTrackModal] Using system auto-selection`); } } }, [showAudioModal, selectedAudioTrack, vlcAudioTracks]); @@ -146,15 +151,16 @@ export const AudioTrackModal: React.FC = ({ {vlcAudioTracks.map((track) => { - // If no track is selected, show the first track as selected - const isSelected = selectedAudioTrack === track.id || - (selectedAudioTrack === null && track.id === vlcAudioTracks[0]?.id); + // Determine if track is selected + let isSelected = false; + if (selectedAudioTrack?.type === 'index' && selectedAudioTrack.value === track.id) { + isSelected = true; + } else if (selectedAudioTrack?.type === 'system' && track.id === vlcAudioTracks[0]?.id) { + // Show first track as selected when using system selection + isSelected = true; + } - // Check if track uses unsupported codec - const trackName = (track.name || '').toLowerCase(); - const isUnsupported = trackName.includes('truehd') || - trackName.includes('dts') || - trackName.includes('atmos'); + // All tracks are now available for selection return ( = ({ padding: 16, borderWidth: 1, borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)', - opacity: isUnsupported ? 0.5 : 1, }} onPress={() => { - if (isUnsupported) { - if (DEBUG_MODE) { - logger.log(`[AudioTrackModal] Attempted to select unsupported track: ${track.id} (${track.name})`); - } - return; // Don't allow selection of unsupported tracks - } if (DEBUG_MODE) { logger.log(`[AudioTrackModal] Selecting track: ${track.id} (${track.name})`); } - selectAudioTrack(track.id); + selectAudioTrack({ type: SelectedTrackType.INDEX, value: track.id }); // Close modal after selection setTimeout(() => { setShowAudioModal(false); }, 200); }} - activeOpacity={isUnsupported ? 1 : 0.7} + activeOpacity={0.7} > {getTrackDisplayName(track)} - {isUnsupported && ( - - - Unsupported - - - )} {track.language && ( {track.language.toUpperCase()} )} - {isSelected && !isUnsupported && ( + {isSelected && ( )} - {isSelected && isUnsupported && ( - - )} ); diff --git a/src/services/SyncService.ts b/src/services/SyncService.ts index d86d5b83..53d403bd 100644 --- a/src/services/SyncService.ts +++ b/src/services/SyncService.ts @@ -198,7 +198,7 @@ class SyncService { try { const u = await accountService.getCurrentUser(); if (!u) return; - // Compare excluding preinstalled + // Compare excluding preinstalled addons const exclude = new Set(['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3']); const localIds = new Set( (await stremioService.getInstalledAddonsAsync()) @@ -651,7 +651,12 @@ class SyncService { // Always include preinstalled regardless of server try { map.set('com.linvo.cinemeta', await stremioService.getManifest('https://v3-cinemeta.strem.io/manifest.json')); } catch {} - try { map.set('org.stremio.opensubtitlesv3', await stremioService.getManifest('https://opensubtitles-v3.strem.io/manifest.json')); } catch {} + + // Only include OpenSubtitles if user hasn't explicitly removed it + const hasUserRemovedOpenSubtitles = await stremioService.hasUserRemovedAddon('org.stremio.opensubtitlesv3'); + if (!hasUserRemovedOpenSubtitles) { + try { map.set('org.stremio.opensubtitlesv3', await stremioService.getManifest('https://opensubtitles-v3.strem.io/manifest.json')); } catch {} + } (stremioService as any).installedAddons = map; let order = (addons as any[]).map(a => a.addon_id); @@ -661,7 +666,11 @@ class SyncService { else if (idx > 0) { arr.splice(idx, 1); arr.unshift(id); } }; ensureFront(order, 'com.linvo.cinemeta'); - ensureFront(order, 'org.stremio.opensubtitlesv3'); + + // Only ensure OpenSubtitles is in order if user hasn't removed it + if (!hasUserRemovedOpenSubtitles) { + ensureFront(order, 'org.stremio.opensubtitlesv3'); + } // Prefer local order if it exists; otherwise use remote try { const userScope = `@user:${userId}:stremio-addon-order`; @@ -861,9 +870,21 @@ class SyncService { .eq('user_id', userId); if (!rErr && remote) { const localIds = new Set(addons.map((a: any) => a.id)); - const toDelete = (remote as any[]) + const toDeletePromises = (remote as any[]) .map(r => r.addon_id as string) - .filter(id => !localIds.has(id) && id !== 'com.linvo.cinemeta' && id !== 'org.stremio.opensubtitlesv3'); + .map(async id => { + if (localIds.has(id)) return null; // Don't delete if still installed locally + if (id === 'com.linvo.cinemeta') return null; // Never delete Cinemeta + if (id === 'org.stremio.opensubtitlesv3') { + // Don't delete OpenSubtitles if user has explicitly removed it + const userRemoved = await stremioService.hasUserRemovedAddon(id); + return userRemoved ? null : id; + } + return id; // Delete other addons that are no longer installed locally + }); + + const toDeleteResults = await Promise.all(toDeletePromises); + const toDelete = toDeleteResults.filter(id => id !== null); logger.log(`[Sync] push installed_addons deletions=${toDelete.length}`); if (toDelete.length > 0) { const del = await supabase diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 8eec2834..6c882536 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -267,9 +267,11 @@ class StremioService { } } - // Ensure OpenSubtitles v3 is always installed as a pre-installed addon + // Install OpenSubtitles v3 by default unless user has explicitly removed it const opensubsId = 'org.stremio.opensubtitlesv3'; - if (!this.installedAddons.has(opensubsId)) { + const hasUserRemovedOpenSubtitles = await this.hasUserRemovedAddon(opensubsId); + + if (!this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitles) { try { const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json'); this.installedAddons.set(opensubsId, opensubsManifest); @@ -312,7 +314,10 @@ class StremioService { if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId)) { this.addonOrder.push(cinemetaId); } - if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId)) { + + // Only add OpenSubtitles to order if user hasn't removed it + const hasUserRemovedOpenSubtitlesOrder = await this.hasUserRemovedAddon(opensubsId); + if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitlesOrder) { this.addonOrder.push(opensubsId); } @@ -435,6 +440,13 @@ class StremioService { if (manifest && manifest.id) { this.installedAddons.set(manifest.id, manifest); + // If this is OpenSubtitles being reinstalled, remove it from the user removed list + if (manifest.id === 'org.stremio.opensubtitlesv3') { + await this.unmarkAddonAsRemovedByUser(manifest.id); + // Also clean up any storage references + await this.cleanupRemovedAddonFromStorage(manifest.id); + } + // Add to order if not already present (new addons go to the end) if (!this.addonOrder.includes(manifest.id)) { this.addonOrder.push(manifest.id); @@ -460,6 +472,14 @@ class StremioService { this.installedAddons.delete(id); // Remove from order this.addonOrder = this.addonOrder.filter(addonId => addonId !== id); + + // Track if user explicitly removed OpenSubtitles + if (id === 'org.stremio.opensubtitlesv3') { + this.markAddonAsRemovedByUser(id); + // Also remove from any stored addon order to prevent it from being added back + this.cleanupRemovedAddonFromStorage(id); + } + this.saveInstalledAddons(); this.saveAddonOrder(); try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {} @@ -479,7 +499,10 @@ class StremioService { if (!result.find(a => a.id === cinId) && this.installedAddons.has(cinId)) { result.unshift(this.installedAddons.get(cinId)!); } + // Only include OpenSubtitles if user hasn't explicitly removed it if (!result.find(a => a.id === osId) && this.installedAddons.has(osId)) { + // Check if user has removed OpenSubtitles (async check, but we'll handle it synchronously for now) + // For now, we'll rely on the fact that if user removed it, it shouldn't be in installedAddons // Put OpenSubtitles right after Cinemeta if possible, else at start const cinIdx = result.findIndex(a => a.id === cinId); const osManifest = this.installedAddons.get(osId)!; @@ -502,6 +525,77 @@ class StremioService { return id === 'com.linvo.cinemeta'; } + // Check if user has explicitly removed an addon + async hasUserRemovedAddon(addonId: string): Promise { + try { + const removedAddons = await AsyncStorage.getItem('user_removed_addons'); + if (!removedAddons) return false; + const removedList = JSON.parse(removedAddons); + return Array.isArray(removedList) && removedList.includes(addonId); + } catch (error) { + return false; + } + } + + // Mark an addon as removed by user + private async markAddonAsRemovedByUser(addonId: string): Promise { + try { + const removedAddons = await AsyncStorage.getItem('user_removed_addons'); + let removedList = removedAddons ? JSON.parse(removedAddons) : []; + if (!Array.isArray(removedList)) removedList = []; + + if (!removedList.includes(addonId)) { + removedList.push(addonId); + await AsyncStorage.setItem('user_removed_addons', JSON.stringify(removedList)); + } + } catch (error) { + // Silently fail - this is not critical functionality + } + } + + // Remove an addon from the user removed list (allows reinstallation) + async unmarkAddonAsRemovedByUser(addonId: string): Promise { + try { + const removedAddons = await AsyncStorage.getItem('user_removed_addons'); + if (!removedAddons) return; + + let removedList = JSON.parse(removedAddons); + if (!Array.isArray(removedList)) return; + + const updatedList = removedList.filter(id => id !== addonId); + await AsyncStorage.setItem('user_removed_addons', JSON.stringify(updatedList)); + } catch (error) { + // Silently fail - this is not critical functionality + } + } + + // Clean up removed addon from all storage locations + private async cleanupRemovedAddonFromStorage(addonId: string): Promise { + try { + const scope = (await AsyncStorage.getItem('@user:current')) || 'local'; + + // Remove from all possible addon order storage keys + const keys = [ + `@user:${scope}:${this.ADDON_ORDER_KEY}`, + this.ADDON_ORDER_KEY, + `@user:local:${this.ADDON_ORDER_KEY}` + ]; + + for (const key of keys) { + const storedOrder = await AsyncStorage.getItem(key); + if (storedOrder) { + const order = JSON.parse(storedOrder); + if (Array.isArray(order)) { + const updatedOrder = order.filter(id => id !== addonId); + await AsyncStorage.setItem(key, JSON.stringify(updatedOrder)); + } + } + } + } catch (error) { + // Silently fail - this is not critical functionality + } + } + private formatId(id: string): string { return id.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase(); }