added subtitle addon support

This commit is contained in:
tapframe 2025-08-08 15:49:29 +05:30
parent 583db67853
commit 7d9f8fba86
4 changed files with 300 additions and 243 deletions

View file

@ -15,14 +15,13 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings'; import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
import { useMetadata } from '../../hooks/useMetadata'; import { useMetadata } from '../../hooks/useMetadata';
import { useSettings } from '../../hooks/useSettings'; import { useSettings } from '../../hooks/useSettings';
import { testVideoStreamUrl } from '../../utils/httpInterceptor';
import { stremioService, Subtitle } from '../../services/stremioService';
import { import {
DEFAULT_SUBTITLE_SIZE, DEFAULT_SUBTITLE_SIZE,
AudioTrack, AudioTrack,
TextTrack, TextTrack,
ResizeModeType, ResizeModeType,
WyzieSubtitle,
SubtitleCue, SubtitleCue,
RESUME_PREF_KEY, RESUME_PREF_KEY,
RESUME_PREF, RESUME_PREF,
@ -36,6 +35,8 @@ import ResumeOverlay from './modals/ResumeOverlay';
import PlayerControls from './controls/PlayerControls'; import PlayerControls from './controls/PlayerControls';
import CustomSubtitles from './subtitles/CustomSubtitles'; import CustomSubtitles from './subtitles/CustomSubtitles';
import { SourcesModal } from './modals/SourcesModal'; import { SourcesModal } from './modals/SourcesModal';
import { stremioService } from '../../services/stremioService';
import axios from 'axios';
// Map VLC resize modes to react-native-video resize modes // Map VLC resize modes to react-native-video resize modes
const getVideoResizeMode = (resizeMode: ResizeModeType) => { const getVideoResizeMode = (resizeMode: ResizeModeType) => {
@ -145,11 +146,12 @@ const AndroidVideoPlayer: React.FC = () => {
const pinchRef = useRef<PinchGestureHandler>(null); const pinchRef = useRef<PinchGestureHandler>(null);
const [customSubtitles, setCustomSubtitles] = useState<SubtitleCue[]>([]); const [customSubtitles, setCustomSubtitles] = useState<SubtitleCue[]>([]);
const [currentSubtitle, setCurrentSubtitle] = useState<string>(''); const [currentSubtitle, setCurrentSubtitle] = useState<string>('');
const [customSubtitleVersion, setCustomSubtitleVersion] = useState<number>(0);
const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE); const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE);
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true); const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false); const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false); const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
const [availableSubtitles, setAvailableSubtitles] = useState<Subtitle[]>([]); const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false); const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false);
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false); const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false);
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false); const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
@ -403,8 +405,6 @@ const AndroidVideoPlayer: React.FC = () => {
saveWatchProgress(); saveWatchProgress();
}, syncInterval); }, syncInterval);
// Removed excessive logging for watch progress save interval
setProgressSaveInterval(interval); setProgressSaveInterval(interval);
return () => { return () => {
clearInterval(interval); clearInterval(interval);
@ -498,8 +498,6 @@ const AndroidVideoPlayer: React.FC = () => {
const onLoad = (data: any) => { const onLoad = (data: any) => {
try { try {
// HTTP response logging removed
if (DEBUG_MODE) { if (DEBUG_MODE) {
logger.log('[AndroidVideoPlayer] Video loaded:', data); logger.log('[AndroidVideoPlayer] Video loaded:', data);
} }
@ -622,7 +620,7 @@ const AndroidVideoPlayer: React.FC = () => {
NativeModules.StatusBarManager.setHidden(true); NativeModules.StatusBarManager.setHidden(true);
} }
} catch (error) { } catch (error) {
// Immersive mode error - silently handled console.log('Immersive mode error:', error);
} }
} }
}; };
@ -715,8 +713,6 @@ const AndroidVideoPlayer: React.FC = () => {
const handleError = (error: any) => { const handleError = (error: any) => {
try { try {
// HTTP error response logging removed
logger.error('AndroidVideoPlayer error: ', error); logger.error('AndroidVideoPlayer error: ', error);
// Early return if component is unmounted to prevent iOS crashes // Early return if component is unmounted to prevent iOS crashes
@ -891,100 +887,183 @@ const AndroidVideoPlayer: React.FC = () => {
} }
setIsLoadingSubtitleList(true); setIsLoadingSubtitleList(true);
try { try {
// Determine content type and ID format for Stremio // Fetch from installed OpenSubtitles v3 addon via Stremio only
let contentType = 'movie'; const stremioType = type === 'series' ? 'series' : 'movie';
let contentId = targetImdbId; const stremioVideoId = stremioType === 'series' && season && episode
let videoId: string | undefined; ? `series:${targetImdbId}:${season}:${episode}`
: undefined;
if (season && episode) { const stremioResults = await stremioService.getSubtitles(stremioType, targetImdbId, stremioVideoId);
contentType = 'series'; const stremioSubs: WyzieSubtitle[] = (stremioResults || []).map(sub => ({
videoId = `series:${targetImdbId}:${season}:${episode}`; id: sub.id || `${sub.lang}-${sub.url}`,
} url: sub.url,
flagUrl: '',
logger.log(`[AndroidVideoPlayer] Fetching subtitles for ${contentType}: ${contentId}${videoId ? ` (${videoId})` : ''}`); format: 'srt',
encoding: 'utf-8',
const subtitles = await stremioService.getSubtitles(contentType, contentId, videoId); media: 'opensubtitles',
display: sub.lang || 'Unknown',
// Remove duplicates based on language language: (sub.lang || '').toLowerCase(),
const uniqueSubtitles = subtitles.reduce((acc: Subtitle[], current: Subtitle) => { isHearingImpaired: false,
const exists = acc.find((item: Subtitle) => item.lang === current.lang); source: sub.addonName || 'OpenSubtitles v3',
}));
// De-duplicate by language
const uniqueSubtitles = stremioSubs.reduce((acc, current) => {
const exists = acc.find(item => item.language === current.language);
if (!exists) { if (!exists) {
acc.push(current); acc.push(current);
} }
return acc; return acc;
}, [] as Subtitle[]); }, [] as WyzieSubtitle[]);
uniqueSubtitles.sort((a, b) => a.display.localeCompare(b.display));
// Sort by language
uniqueSubtitles.sort((a: Subtitle, b: Subtitle) => a.lang.localeCompare(b.lang));
setAvailableSubtitles(uniqueSubtitles); setAvailableSubtitles(uniqueSubtitles);
if (autoSelectEnglish) { if (autoSelectEnglish) {
const englishSubtitle = uniqueSubtitles.find((sub: Subtitle) => const englishSubtitle = uniqueSubtitles.find(sub =>
sub.lang.toLowerCase() === 'eng' || sub.language.toLowerCase() === 'eng' ||
sub.lang.toLowerCase() === 'en' || sub.language.toLowerCase() === 'en' ||
sub.lang.toLowerCase().includes('english') sub.display.toLowerCase().includes('english')
); );
if (englishSubtitle) { if (englishSubtitle) {
loadStremioSubtitle(englishSubtitle); loadWyzieSubtitle(englishSubtitle);
return; return;
} }
} }
if (!autoSelectEnglish) {
if (!autoSelectEnglish && uniqueSubtitles.length > 0) {
setShowSubtitleLanguageModal(true); setShowSubtitleLanguageModal(true);
} }
} catch (error) { } catch (error) {
logger.error('[AndroidVideoPlayer] Error fetching subtitles from Stremio addons:', error); logger.error('[AndroidVideoPlayer] Error fetching subtitles from OpenSubtitles addon:', error);
} finally { } finally {
setIsLoadingSubtitleList(false); setIsLoadingSubtitleList(false);
} }
}; };
const loadStremioSubtitle = async (subtitle: Subtitle) => { const loadWyzieSubtitle = async (subtitle: WyzieSubtitle) => {
console.log('[AndroidVideoPlayer] Starting subtitle load, setting isLoadingSubtitles to true'); logger.log(`[AndroidVideoPlayer] Subtitle click received: id=${subtitle.id}, lang=${subtitle.language}, url=${subtitle.url}`);
setShowSubtitleLanguageModal(false); setShowSubtitleLanguageModal(false);
logger.log('[AndroidVideoPlayer] setShowSubtitleLanguageModal(false)');
setIsLoadingSubtitles(true); setIsLoadingSubtitles(true);
logger.log('[AndroidVideoPlayer] isLoadingSubtitles -> true');
try { try {
const response = await fetch(subtitle.url); logger.log('[AndroidVideoPlayer] Fetching subtitle SRT start');
const srtContent = await response.text(); let srtContent = '';
try {
const axiosResp = await axios.get(subtitle.url, {
timeout: 10000,
headers: {
'Accept': 'text/plain, */*',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Nuvio/1.0'
},
responseType: 'text',
transitional: {
clarifyTimeoutError: true
}
});
srtContent = typeof axiosResp.data === 'string' ? axiosResp.data : String(axiosResp.data || '');
} catch (axiosErr: any) {
logger.warn('[AndroidVideoPlayer] Axios subtitle fetch failed, falling back to fetch()', {
message: axiosErr?.message,
code: axiosErr?.code
});
// Fallback with explicit timeout using AbortController
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const resp = await fetch(subtitle.url, { signal: controller.signal });
srtContent = await resp.text();
} finally {
clearTimeout(timeoutId);
}
}
logger.log(`[AndroidVideoPlayer] Fetching subtitle SRT done, size=${srtContent.length}`);
const parsedCues = parseSRT(srtContent); const parsedCues = parseSRT(srtContent);
logger.log(`[AndroidVideoPlayer] Parsed cues count=${parsedCues.length}`);
// Force a microtask delay before updating subtitle state // iOS AVPlayer workaround: clear subtitle state first, then apply
await new Promise(resolve => setTimeout(resolve, 50)); if (Platform.OS === 'ios') {
logger.log('[AndroidVideoPlayer] iOS detected; clearing subtitle state before apply');
setCustomSubtitles(parsedCues); // Immediately stop spinner so UI doesn't get stuck
setUseCustomSubtitles(true); setIsLoadingSubtitles(false);
setSelectedTextTrack(-1); logger.log('[AndroidVideoPlayer] isLoadingSubtitles -> false (early stop for iOS)');
logger.log(`[AndroidVideoPlayer] Loaded subtitle: ${subtitle.lang} from ${subtitle.addonName}`); // Step 1: Clear any existing subtitle state
console.log('[AndroidVideoPlayer] Subtitle loaded successfully, about to seek'); setUseCustomSubtitles(false);
logger.log('[AndroidVideoPlayer] useCustomSubtitles -> false');
// Force a state update by triggering a seek setCustomSubtitles([]);
if (videoRef.current && duration > 0 && !isSeeking.current) { logger.log('[AndroidVideoPlayer] customSubtitles -> []');
const currentPos = currentTime; setSelectedTextTrack(-1);
console.log(`[AndroidVideoPlayer] Forcing a micro-seek to refresh subtitle state from ${currentPos}s`); logger.log('[AndroidVideoPlayer] selectedTextTrack -> -1');
seekToTime(currentPos);
// Wait for seek to complete before clearing loading state // Step 2: Apply immediately (no scheduling), then do a small micro-nudge
setTimeout(() => { logger.log('[AndroidVideoPlayer] Applying parsed cues immediately (iOS)');
console.log('[AndroidVideoPlayer] Clearing isLoadingSubtitles after seek timeout'); setCustomSubtitles(parsedCues);
setIsLoadingSubtitles(false); logger.log('[AndroidVideoPlayer] customSubtitles <- parsedCues');
}, 600); // Wait longer than seekToTime's internal timeout (500ms) setUseCustomSubtitles(true);
logger.log('[AndroidVideoPlayer] useCustomSubtitles -> true');
setSelectedTextTrack(-1);
logger.log('[AndroidVideoPlayer] selectedTextTrack -> -1 (disable native while using custom)');
setCustomSubtitleVersion(v => v + 1);
logger.log('[AndroidVideoPlayer] customSubtitleVersion incremented');
// Immediately set current subtitle based on currentTime to avoid waiting for next onProgress
try {
const cueNow = parsedCues.find(cue => currentTime >= cue.start && currentTime <= cue.end);
const textNow = cueNow ? cueNow.text : '';
setCurrentSubtitle(textNow);
logger.log('[AndroidVideoPlayer] currentSubtitle set immediately after apply (iOS)');
} catch (e) {
logger.error('[AndroidVideoPlayer] Error setting immediate subtitle', e);
}
// Micro-seek nudge to force AVPlayer to refresh rendering
try {
if (videoRef.current && duration > 0) {
const wasPaused = paused;
const original = currentTime;
const forward = Math.min(original + 0.05, Math.max(duration - 0.1, 0));
logger.log('[AndroidVideoPlayer] Performing micro-seek nudge (iOS)', { original, forward });
if (wasPaused) {
setPaused(false);
}
// Give state a moment to apply before seeking
setTimeout(() => {
try {
videoRef.current?.seek(forward);
setTimeout(() => {
videoRef.current?.seek(original);
if (wasPaused) {
setPaused(true);
}
logger.log('[AndroidVideoPlayer] Micro-seek nudge complete (iOS)');
}, 150);
} catch (e) {
logger.warn('[AndroidVideoPlayer] Inner micro-seek failed (iOS)', e);
if (wasPaused) setPaused(true);
}
}, 50);
}
} catch(e) {
logger.warn('[AndroidVideoPlayer] Outer micro-seek failed (iOS)', e);
}
} else { } else {
console.warn(`[AndroidVideoPlayer] Cannot seek to refresh subtitles: videoRef=${!!videoRef.current}, duration=${duration}, seeking=${isSeeking.current}`); // Android works immediately
// Clear loading state immediately if we can't seek setCustomSubtitles(parsedCues);
setTimeout(() => { logger.log('[AndroidVideoPlayer] (Android) customSubtitles <- parsedCues');
setIsLoadingSubtitles(false); setUseCustomSubtitles(true);
console.log('[AndroidVideoPlayer] isLoadingSubtitles set to false (no seek)'); logger.log('[AndroidVideoPlayer] (Android) useCustomSubtitles -> true');
}, 100); setSelectedTextTrack(-1);
logger.log('[AndroidVideoPlayer] (Android) selectedTextTrack -> -1');
setIsLoadingSubtitles(false);
logger.log('[AndroidVideoPlayer] (Android) isLoadingSubtitles -> false');
try {
const cueNow = parsedCues.find(cue => currentTime >= cue.start && currentTime <= cue.end);
const textNow = cueNow ? cueNow.text : '';
setCurrentSubtitle(textNow);
logger.log('[AndroidVideoPlayer] currentSubtitle set immediately after apply (Android)');
} catch {}
} }
} catch (error) { } catch (error) {
logger.error('[AndroidVideoPlayer] Error loading Stremio subtitle:', error); logger.error('[AndroidVideoPlayer] Error loading Wyzie subtitle:', error);
console.log('[AndroidVideoPlayer] Subtitle loading failed:', error); setIsLoadingSubtitles(false);
// Clear loading state on error logger.log('[AndroidVideoPlayer] isLoadingSubtitles -> false (error path)');
setTimeout(() => {
setIsLoadingSubtitles(false);
console.log('[AndroidVideoPlayer] isLoadingSubtitles set to false (error)');
}, 100);
} }
}; };
@ -996,7 +1075,6 @@ const AndroidVideoPlayer: React.FC = () => {
// Send a forced pause update to Trakt immediately when user pauses // Send a forced pause update to Trakt immediately when user pauses
if (newPausedState && duration > 0) { if (newPausedState && duration > 0) {
traktAutosync.handleProgressUpdate(currentTime, duration, true); traktAutosync.handleProgressUpdate(currentTime, duration, true);
logger.log('[AndroidVideoPlayer] Sent forced pause update to Trakt');
} }
} }
}; };
@ -1031,10 +1109,8 @@ const AndroidVideoPlayer: React.FC = () => {
currentTime >= cue.start && currentTime <= cue.end currentTime >= cue.start && currentTime <= cue.end
); );
const newSubtitle = currentCue ? currentCue.text : ''; const newSubtitle = currentCue ? currentCue.text : '';
if (newSubtitle !== currentSubtitle) { setCurrentSubtitle(newSubtitle);
setCurrentSubtitle(newSubtitle); }, [currentTime, customSubtitles, useCustomSubtitles]);
}
}, [currentTime, customSubtitles, useCustomSubtitles, currentSubtitle]);
useEffect(() => { useEffect(() => {
loadSubtitleSize(); loadSubtitleSize();
@ -1097,23 +1173,6 @@ const AndroidVideoPlayer: React.FC = () => {
} }
}, [pendingSeek, isPlayerReady, isVideoLoaded, duration]); }, [pendingSeek, isPlayerReady, isVideoLoaded, duration]);
// HTTP stream testing with logging
useEffect(() => {
if (currentStreamUrl && currentStreamUrl.trim() !== '') {
const testStream = async () => {
try {
// Stream testing without verbose logging
await testVideoStreamUrl(currentStreamUrl, headers || {});
} catch (error) {
// Stream test failed silently
}
};
// Test the stream URL when it changes
testStream();
}
}, [currentStreamUrl, headers]);
const handleSelectStream = async (newStream: any) => { const handleSelectStream = async (newStream: any) => {
if (newStream.url === currentStreamUrl) { if (newStream.url === currentStreamUrl) {
setShowSourcesModal(false); setShowSourcesModal(false);
@ -1315,16 +1374,25 @@ const AndroidVideoPlayer: React.FC = () => {
headers: headers headers: headers
} : { uri: currentStreamUrl }; } : { uri: currentStreamUrl };
// HTTP request logging removed // HTTP request logging removed; source prepared
return sourceWithHeaders; return sourceWithHeaders;
})()} })()}
paused={paused} paused={paused}
onProgress={handleProgress} onProgress={handleProgress}
onLoad={onLoad} onLoad={(e) => {
logger.log('[AndroidVideoPlayer] onLoad fired', { duration: e?.duration });
onLoad(e);
}}
onEnd={onEnd} onEnd={onEnd}
onError={handleError} onError={(err) => {
onBuffer={onBuffer} logger.error('[AndroidVideoPlayer] onError', err);
handleError(err);
}}
onBuffer={(buf) => {
logger.log('[AndroidVideoPlayer] onBuffer', buf);
onBuffer(buf);
}}
resizeMode={getVideoResizeMode(resizeMode)} resizeMode={getVideoResizeMode(resizeMode)}
selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined} selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined}
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)} selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
@ -1376,6 +1444,7 @@ const AndroidVideoPlayer: React.FC = () => {
/> />
<CustomSubtitles <CustomSubtitles
key={customSubtitleVersion}
useCustomSubtitles={useCustomSubtitles} useCustomSubtitles={useCustomSubtitles}
currentSubtitle={currentSubtitle} currentSubtitle={currentSubtitle}
subtitleSize={subtitleSize} subtitleSize={subtitleSize}
@ -1418,7 +1487,7 @@ const AndroidVideoPlayer: React.FC = () => {
subtitleSize={subtitleSize} subtitleSize={subtitleSize}
subtitleBackground={subtitleBackground} subtitleBackground={subtitleBackground}
fetchAvailableSubtitles={fetchAvailableSubtitles} fetchAvailableSubtitles={fetchAvailableSubtitles}
loadStremioSubtitle={loadStremioSubtitle} loadWyzieSubtitle={loadWyzieSubtitle}
selectTextTrack={selectTextTrack} selectTextTrack={selectTextTrack}
increaseSubtitleSize={increaseSubtitleSize} increaseSubtitleSize={increaseSubtitleSize}
decreaseSubtitleSize={decreaseSubtitleSize} decreaseSubtitleSize={decreaseSubtitleSize}

View file

@ -17,13 +17,13 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings'; import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
import { useMetadata } from '../../hooks/useMetadata'; import { useMetadata } from '../../hooks/useMetadata';
import { useSettings } from '../../hooks/useSettings'; import { useSettings } from '../../hooks/useSettings';
import { stremioService, Subtitle } from '../../services/stremioService';
import { import {
DEFAULT_SUBTITLE_SIZE, DEFAULT_SUBTITLE_SIZE,
AudioTrack, AudioTrack,
TextTrack, TextTrack,
ResizeModeType, ResizeModeType,
WyzieSubtitle,
SubtitleCue, SubtitleCue,
RESUME_PREF_KEY, RESUME_PREF_KEY,
RESUME_PREF, RESUME_PREF,
@ -37,6 +37,7 @@ import ResumeOverlay from './modals/ResumeOverlay';
import PlayerControls from './controls/PlayerControls'; import PlayerControls from './controls/PlayerControls';
import CustomSubtitles from './subtitles/CustomSubtitles'; import CustomSubtitles from './subtitles/CustomSubtitles';
import { SourcesModal } from './modals/SourcesModal'; import { SourcesModal } from './modals/SourcesModal';
import { stremioService } from '../../services/stremioService';
const VideoPlayer: React.FC = () => { const VideoPlayer: React.FC = () => {
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>(); const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
@ -51,8 +52,8 @@ const VideoPlayer: React.FC = () => {
// Use AndroidVideoPlayer for: // Use AndroidVideoPlayer for:
// - Android devices // - Android devices
// - Xprime streams on any platform // - Xprime streams on any platform
// - MKV files on iOS are now handled by VideoPlayer.tsx (VLCPlayer) // - Non-MKV files on iOS
if (Platform.OS === 'android' || isXprimeStream) { if (Platform.OS === 'android' || isXprimeStream || (Platform.OS === 'ios' && !isMkvFile)) {
return <AndroidVideoPlayer />; return <AndroidVideoPlayer />;
} }
@ -160,7 +161,7 @@ const VideoPlayer: React.FC = () => {
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true); const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false); const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false); const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
const [availableSubtitles, setAvailableSubtitles] = useState<Subtitle[]>([]); const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false); const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false);
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false); const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false);
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false); const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
@ -434,8 +435,6 @@ const VideoPlayer: React.FC = () => {
saveWatchProgress(); saveWatchProgress();
}, syncInterval); }, syncInterval);
// Removed excessive logging for watch progress save interval
setProgressSaveInterval(interval); setProgressSaveInterval(interval);
return () => { return () => {
clearInterval(interval); clearInterval(interval);
@ -470,7 +469,6 @@ const VideoPlayer: React.FC = () => {
// Send a forced pause update to Trakt immediately when user pauses // Send a forced pause update to Trakt immediately when user pauses
if (duration > 0) { if (duration > 0) {
traktAutosync.handleProgressUpdate(currentTime, duration, true); traktAutosync.handleProgressUpdate(currentTime, duration, true);
logger.log('[VideoPlayer] Sent forced pause update to Trakt');
} }
} }
}; };
@ -567,7 +565,6 @@ const VideoPlayer: React.FC = () => {
const onLoad = (data: any) => { const onLoad = (data: any) => {
try { try {
if (DEBUG_MODE) { if (DEBUG_MODE) {
logger.log('[VideoPlayer] Video loaded:', data); logger.log('[VideoPlayer] Video loaded:', data);
} }
@ -797,8 +794,6 @@ const VideoPlayer: React.FC = () => {
}; };
const handleError = (error: any) => { const handleError = (error: any) => {
// HTTP error response logging removed
logger.error('[VideoPlayer] Playback Error:', error); logger.error('[VideoPlayer] Playback Error:', error);
// Format error details for user display // Format error details for user display
@ -907,56 +902,57 @@ const VideoPlayer: React.FC = () => {
} }
setIsLoadingSubtitleList(true); setIsLoadingSubtitleList(true);
try { try {
// Determine content type and ID format for Stremio // Fetch from installed OpenSubtitles v3 addon via Stremio only
let contentType = 'movie'; const stremioType = type === 'series' ? 'series' : 'movie';
let contentId = targetImdbId; const stremioVideoId = stremioType === 'series' && season && episode
let videoId: string | undefined; ? `series:${targetImdbId}:${season}:${episode}`
: undefined;
if (season && episode) { const stremioResults = await stremioService.getSubtitles(stremioType, targetImdbId, stremioVideoId);
contentType = 'series'; const stremioSubs: WyzieSubtitle[] = (stremioResults || []).map(sub => ({
videoId = `series:${targetImdbId}:${season}:${episode}`; id: sub.id || `${sub.lang}-${sub.url}`,
} url: sub.url,
flagUrl: '',
logger.log(`[VideoPlayer] Fetching subtitles for ${contentType}: ${contentId}${videoId ? ` (${videoId})` : ''}`); format: 'srt',
encoding: 'utf-8',
const subtitles = await stremioService.getSubtitles(contentType, contentId, videoId); media: 'opensubtitles',
display: sub.lang || 'Unknown',
// Remove duplicates based on language language: (sub.lang || '').toLowerCase(),
const uniqueSubtitles = subtitles.reduce((acc: Subtitle[], current: Subtitle) => { isHearingImpaired: false,
const exists = acc.find((item: Subtitle) => item.lang === current.lang); source: sub.addonName || 'OpenSubtitles v3',
}));
// De-duplicate by language
const uniqueSubtitles = stremioSubs.reduce((acc, current) => {
const exists = acc.find(item => item.language === current.language);
if (!exists) { if (!exists) {
acc.push(current); acc.push(current);
} }
return acc; return acc;
}, [] as Subtitle[]); }, [] as WyzieSubtitle[]);
uniqueSubtitles.sort((a, b) => a.display.localeCompare(b.display));
// Sort by language
uniqueSubtitles.sort((a: Subtitle, b: Subtitle) => a.lang.localeCompare(b.lang));
setAvailableSubtitles(uniqueSubtitles); setAvailableSubtitles(uniqueSubtitles);
if (autoSelectEnglish) { if (autoSelectEnglish) {
const englishSubtitle = uniqueSubtitles.find((sub: Subtitle) => const englishSubtitle = uniqueSubtitles.find(sub =>
sub.lang.toLowerCase() === 'eng' || sub.language.toLowerCase() === 'eng' ||
sub.lang.toLowerCase() === 'en' || sub.language.toLowerCase() === 'en' ||
sub.lang.toLowerCase().includes('english') sub.display.toLowerCase().includes('english')
); );
if (englishSubtitle) { if (englishSubtitle) {
loadStremioSubtitle(englishSubtitle); loadWyzieSubtitle(englishSubtitle);
return; return;
} }
} }
if (!autoSelectEnglish) {
if (!autoSelectEnglish && uniqueSubtitles.length > 0) {
setShowSubtitleLanguageModal(true); setShowSubtitleLanguageModal(true);
} }
} catch (error) { } catch (error) {
logger.error('[VideoPlayer] Error fetching subtitles from Stremio addons:', error); logger.error('[VideoPlayer] Error fetching subtitles from OpenSubtitles addon:', error);
} finally { } finally {
setIsLoadingSubtitleList(false); setIsLoadingSubtitleList(false);
} }
}; };
const loadStremioSubtitle = async (subtitle: Subtitle) => { const loadWyzieSubtitle = async (subtitle: WyzieSubtitle) => {
setShowSubtitleLanguageModal(false); setShowSubtitleLanguageModal(false);
setIsLoadingSubtitles(true); setIsLoadingSubtitles(true);
try { try {
@ -966,14 +962,10 @@ const VideoPlayer: React.FC = () => {
setCustomSubtitles(parsedCues); setCustomSubtitles(parsedCues);
setUseCustomSubtitles(true); setUseCustomSubtitles(true);
setSelectedTextTrack(-1); setSelectedTextTrack(-1);
logger.log(`[VideoPlayer] Loaded subtitle: ${subtitle.lang} from ${subtitle.addonName}`);
} catch (error) { } catch (error) {
logger.error('[VideoPlayer] Error loading Stremio subtitle:', error); logger.error('[VideoPlayer] Error loading Wyzie subtitle:', error);
} finally { } finally {
// Add a small delay to ensure state updates are processed setIsLoadingSubtitles(false);
setTimeout(() => {
setIsLoadingSubtitles(false);
}, 100);
} }
}; };
@ -1013,10 +1005,8 @@ const VideoPlayer: React.FC = () => {
currentTime >= cue.start && currentTime <= cue.end currentTime >= cue.start && currentTime <= cue.end
); );
const newSubtitle = currentCue ? currentCue.text : ''; const newSubtitle = currentCue ? currentCue.text : '';
if (newSubtitle !== currentSubtitle) { setCurrentSubtitle(newSubtitle);
setCurrentSubtitle(newSubtitle); }, [currentTime, customSubtitles, useCustomSubtitles]);
}
}, [currentTime, customSubtitles, useCustomSubtitles, currentSubtitle]);
useEffect(() => { useEffect(() => {
loadSubtitleSize(); loadSubtitleSize();
@ -1285,17 +1275,15 @@ const VideoPlayer: React.FC = () => {
<VLCPlayer <VLCPlayer
ref={vlcRef} ref={vlcRef}
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]} style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
source={(() => { source={(() => {
// FORCEFULLY use headers from route params if available - no filtering or modification // FORCEFULLY use headers from route params if available - no filtering or modification
const sourceWithHeaders = headers ? { const sourceWithHeaders = headers ? {
uri: currentStreamUrl, uri: currentStreamUrl,
headers: headers headers: headers
} : { uri: currentStreamUrl }; } : { uri: currentStreamUrl };
// HTTP request logging removed return sourceWithHeaders;
})()}
return sourceWithHeaders;
})()}
paused={paused} paused={paused}
onProgress={handleProgress} onProgress={handleProgress}
onLoad={onLoad} onLoad={onLoad}
@ -1388,7 +1376,7 @@ const VideoPlayer: React.FC = () => {
subtitleSize={subtitleSize} subtitleSize={subtitleSize}
subtitleBackground={subtitleBackground} subtitleBackground={subtitleBackground}
fetchAvailableSubtitles={fetchAvailableSubtitles} fetchAvailableSubtitles={fetchAvailableSubtitles}
loadStremioSubtitle={loadStremioSubtitle} loadWyzieSubtitle={loadWyzieSubtitle}
selectTextTrack={selectTextTrack} selectTextTrack={selectTextTrack}
increaseSubtitleSize={increaseSubtitleSize} increaseSubtitleSize={increaseSubtitleSize}
decreaseSubtitleSize={decreaseSubtitleSize} decreaseSubtitleSize={decreaseSubtitleSize}

View file

@ -8,8 +8,7 @@ import Animated, {
SlideOutRight, SlideOutRight,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { styles } from '../utils/playerStyles'; import { styles } from '../utils/playerStyles';
import { SubtitleCue } from '../utils/playerTypes'; import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
import { Subtitle } from '../../../services/stremioService';
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils'; import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
interface SubtitleModalsProps { interface SubtitleModalsProps {
@ -20,14 +19,14 @@ interface SubtitleModalsProps {
isLoadingSubtitleList: boolean; isLoadingSubtitleList: boolean;
isLoadingSubtitles: boolean; isLoadingSubtitles: boolean;
customSubtitles: SubtitleCue[]; customSubtitles: SubtitleCue[];
availableSubtitles: Subtitle[]; availableSubtitles: WyzieSubtitle[];
vlcTextTracks: Array<{id: number, name: string, language?: string}>; vlcTextTracks: Array<{id: number, name: string, language?: string}>;
selectedTextTrack: number; selectedTextTrack: number;
useCustomSubtitles: boolean; useCustomSubtitles: boolean;
subtitleSize: number; subtitleSize: number;
subtitleBackground: boolean; subtitleBackground: boolean;
fetchAvailableSubtitles: () => void; fetchAvailableSubtitles: () => void;
loadStremioSubtitle: (subtitle: Subtitle) => void; loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void;
selectTextTrack: (trackId: number) => void; selectTextTrack: (trackId: number) => void;
increaseSubtitleSize: () => void; increaseSubtitleSize: () => void;
decreaseSubtitleSize: () => void; decreaseSubtitleSize: () => void;
@ -52,7 +51,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
subtitleSize, subtitleSize,
subtitleBackground, subtitleBackground,
fetchAvailableSubtitles, fetchAvailableSubtitles,
loadStremioSubtitle, loadWyzieSubtitle,
selectTextTrack, selectTextTrack,
increaseSubtitleSize, increaseSubtitleSize,
decreaseSubtitleSize, decreaseSubtitleSize,
@ -60,7 +59,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
}) => { }) => {
// Track which specific online subtitle is currently loaded // Track which specific online subtitle is currently loaded
const [selectedOnlineSubtitleId, setSelectedOnlineSubtitleId] = React.useState<string | null>(null); const [selectedOnlineSubtitleId, setSelectedOnlineSubtitleId] = React.useState<string | null>(null);
// Track which subtitle is currently being loaded // Track which online subtitle is currently loading to show spinner per-item
const [loadingSubtitleId, setLoadingSubtitleId] = React.useState<string | null>(null); const [loadingSubtitleId, setLoadingSubtitleId] = React.useState<string | null>(null);
React.useEffect(() => { React.useEffect(() => {
@ -76,30 +75,25 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
} }
}, [useCustomSubtitles]); }, [useCustomSubtitles]);
// Clear loading state when subtitles have finished loading
React.useEffect(() => {
if (!isLoadingSubtitles) {
setLoadingSubtitleId(null);
}
}, [isLoadingSubtitles]);
// Only OpenSubtitles are provided now; render as a single list
const handleClose = () => { const handleClose = () => {
setShowSubtitleModal(false); setShowSubtitleModal(false);
}; };
const handleLoadStremioSubtitle = (subtitle: Subtitle) => { const handleLoadWyzieSubtitle = (subtitle: WyzieSubtitle) => {
console.log('[SubtitleModals] Starting to load subtitle:', subtitle.id);
setLoadingSubtitleId(subtitle.id);
setSelectedOnlineSubtitleId(subtitle.id); setSelectedOnlineSubtitleId(subtitle.id);
loadStremioSubtitle(subtitle); setLoadingSubtitleId(subtitle.id);
loadWyzieSubtitle(subtitle);
}; };
// Clear loading state when subtitle loading is complete
React.useEffect(() => {
console.log('[SubtitleModals] isLoadingSubtitles changed:', isLoadingSubtitles);
if (!isLoadingSubtitles) {
console.log('[SubtitleModals] Clearing loadingSubtitleId');
// Force clear loading state with a small delay to ensure proper re-render
setTimeout(() => {
setLoadingSubtitleId(null);
console.log('[SubtitleModals] loadingSubtitleId cleared');
}, 50);
}
}, [isLoadingSubtitles]);
// Main subtitle menu // Main subtitle menu
const renderSubtitleMenu = () => { const renderSubtitleMenu = () => {
if (!showSubtitleModal) return null; if (!showSubtitleModal) return null;
@ -398,57 +392,8 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{availableSubtitles.length > 0 ? ( {(availableSubtitles.length === 0) && !isLoadingSubtitleList ? (
<View style={{ gap: 8 }}>
{availableSubtitles.map((sub) => {
const isSelected = useCustomSubtitles && selectedOnlineSubtitleId === sub.id;
return (
<TouchableOpacity
key={sub.id}
style={{
backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)',
borderRadius: 16,
padding: 16,
borderWidth: 1,
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
}}
onPress={() => {
handleLoadStremioSubtitle(sub);
}}
activeOpacity={0.7}
disabled={loadingSubtitleId === sub.id}
>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flex: 1 }}>
<Text style={{
color: '#FFFFFF',
fontSize: 15,
fontWeight: '500',
marginBottom: 4,
}}>
{formatLanguage(sub.lang)}
</Text>
<Text style={{
color: 'rgba(255, 255, 255, 0.6)',
fontSize: 13,
}}>
{sub.addonName || 'Stremio Addon'}
</Text>
</View>
{loadingSubtitleId === sub.id ? (
<ActivityIndicator size="small" color="#22C55E" />
) : isSelected ? (
<MaterialIcons name="check" size={20} color="#22C55E" />
) : (
<MaterialIcons name="download" size={20} color="rgba(255,255,255,0.4)" />
)}
</View>
</TouchableOpacity>
);
})}
</View>
) : !isLoadingSubtitleList ? (
<TouchableOpacity <TouchableOpacity
style={{ style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)', backgroundColor: 'rgba(255, 255, 255, 0.05)',
@ -472,7 +417,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
Tap to search online Tap to search online
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
) : ( ) : isLoadingSubtitleList ? (
<View style={{ <View style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)', backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 16, borderRadius: 16,
@ -488,6 +433,47 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
Searching... Searching...
</Text> </Text>
</View> </View>
) : (
<View style={{ gap: 8 }}>
{availableSubtitles.map((sub) => {
const isSelected = useCustomSubtitles && selectedOnlineSubtitleId === sub.id;
return (
<TouchableOpacity
key={sub.id}
style={{
backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)',
borderRadius: 16,
padding: 16,
borderWidth: 1,
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
}}
onPress={() => {
handleLoadWyzieSubtitle(sub);
}}
activeOpacity={0.7}
disabled={isLoadingSubtitles}
>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flex: 1 }}>
<Text style={{ color: '#FFFFFF', fontSize: 15, fontWeight: '500', marginBottom: 4 }}>
{sub.display}
</Text>
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', fontSize: 13 }}>
{formatLanguage(sub.language)}
</Text>
</View>
{(isLoadingSubtitles && loadingSubtitleId === sub.id) ? (
<ActivityIndicator size="small" color="#22C55E" />
) : isSelected ? (
<MaterialIcons name="check" size={20} color="#22C55E" />
) : (
<MaterialIcons name="download" size={20} color="rgba(255,255,255,0.4)" />
)}
</View>
</TouchableOpacity>
);
})}
</View>
)} )}
</View> </View>
@ -557,4 +543,4 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
); );
}; };
export default SubtitleModals; export default SubtitleModals;

View file

@ -71,4 +71,18 @@ export interface SubtitleCue {
start: number; start: number;
end: number; end: number;
text: string; text: string;
} }
// Add interface for Wyzie subtitle API response
export interface WyzieSubtitle {
id: string;
url: string;
flagUrl: string;
format: string;
encoding: string;
media: string;
display: string;
language: string;
isHearingImpaired: boolean;
source: string;
}