/** * Watch Page Controller * Handles VOD (Movies/Series) playback with streaming service-style UI */ class WatchPage { constructor(app) { this.app = app; // Video elements this.video = document.getElementById('watch-video'); this.overlay = document.getElementById('watch-overlay'); // Top bar this.backBtn = document.getElementById('watch-back-btn'); this.titleEl = document.getElementById('watch-title'); this.subtitleEl = document.getElementById('watch-subtitle'); // Controls this.centerPlayBtn = document.getElementById('watch-center-play'); this.playPauseBtn = document.getElementById('watch-play-pause'); this.skipBackBtn = document.getElementById('watch-skip-back'); this.skipFwdBtn = document.getElementById('watch-skip-fwd'); this.muteBtn = document.getElementById('watch-mute'); this.volumeSlider = document.getElementById('watch-volume'); this.fullscreenBtn = document.getElementById('watch-fullscreen'); this.progressSlider = document.getElementById('watch-progress'); this.timeCurrent = document.getElementById('watch-time-current'); this.timeTotal = document.getElementById('watch-time-total'); this.scrollHint = document.getElementById('watch-scroll-hint'); this.loadingSpinner = document.getElementById('watch-loading'); // Next episode this.nextEpisodePanel = document.getElementById('watch-next-episode'); this.nextEpisodeTitle = document.getElementById('next-episode-title'); this.nextCountdown = document.getElementById('next-countdown'); this.nextPlayNowBtn = document.getElementById('next-play-now'); this.nextCancelBtn = document.getElementById('next-cancel'); // Details section this.posterEl = document.getElementById('watch-poster'); this.contentTitleEl = document.getElementById('watch-content-title'); this.yearEl = document.getElementById('watch-year'); this.ratingEl = document.getElementById('watch-rating'); this.durationEl = document.getElementById('watch-duration'); this.descriptionEl = document.getElementById('watch-description'); this.playBtn = document.getElementById('watch-play-btn'); this.playBtnText = document.getElementById('watch-play-btn-text'); this.favoriteBtn = document.getElementById('watch-favorite-btn'); // Recommended / Episodes this.recommendedSection = document.getElementById('watch-recommended'); this.recommendedGrid = document.getElementById('watch-recommended-grid'); this.episodesSection = document.getElementById('watch-episodes'); this.seasonsContainer = document.getElementById('watch-seasons'); // Captions this.captionsBtn = document.getElementById('watch-captions-btn'); this.captionsMenu = document.getElementById('watch-captions-menu'); this.captionsList = document.getElementById('watch-captions-list'); // State this.hls = null; this.content = null; this.contentType = null; // 'movie' or 'series' this.seriesInfo = null; this.currentSeason = null; this.currentEpisode = null; this.isFavorite = false; this.returnPage = null; this.captionsMenuOpen = false; // Overlay timer this.overlayTimeout = null; this.overlayVisible = true; // Next episode this.nextEpisodeTimeout = null; this.nextEpisodeCountdown = 10; this.nextEpisodeInterval = null; this.init(); } init() { // Back button this.backBtn?.addEventListener('click', () => this.goBack()); // Play/Pause this.centerPlayBtn?.addEventListener('click', () => this.togglePlay()); this.playPauseBtn?.addEventListener('click', () => this.togglePlay()); this.video?.addEventListener('click', () => this.togglePlay()); // Skip buttons this.skipBackBtn?.addEventListener('click', () => this.skip(-10)); this.skipFwdBtn?.addEventListener('click', () => this.skip(10)); // Volume this.muteBtn?.addEventListener('click', () => this.toggleMute()); this.volumeSlider?.addEventListener('input', (e) => this.setVolume(e.target.value)); // Fullscreen this.fullscreenBtn?.addEventListener('click', () => this.toggleFullscreen()); // Progress bar this.progressSlider?.addEventListener('input', (e) => this.seek(e.target.value)); // Video events this.video?.addEventListener('timeupdate', () => this.updateProgress()); this.video?.addEventListener('loadedmetadata', () => this.onMetadataLoaded()); this.video?.addEventListener('play', () => this.onPlay()); this.video?.addEventListener('pause', () => this.onPause()); this.video?.addEventListener('ended', () => this.onEnded()); this.video?.addEventListener('error', (e) => this.onError(e)); this.video?.addEventListener('waiting', () => this.showLoading()); this.video?.addEventListener('canplay', () => this.hideLoading()); // Overlay auto-hide const watchSection = document.querySelector('.watch-video-section'); watchSection?.addEventListener('mousemove', () => this.showOverlay()); watchSection?.addEventListener('touchstart', () => this.showOverlay()); // Keyboard shortcuts document.addEventListener('keydown', (e) => this.handleKeyboard(e)); // Details section buttons this.playBtn?.addEventListener('click', () => this.scrollToVideo()); this.favoriteBtn?.addEventListener('click', () => this.toggleFavorite()); // Next episode buttons this.nextPlayNowBtn?.addEventListener('click', () => this.playNextEpisode()); this.nextCancelBtn?.addEventListener('click', () => this.cancelNextEpisode()); // Captions toggle this.captionsBtn?.addEventListener('click', (e) => { e.stopPropagation(); this.toggleCaptionsMenu(); }); // Close captions menu when clicking outside document.addEventListener('click', (e) => { if (this.captionsMenuOpen && !this.captionsMenu?.contains(e.target) && e.target !== this.captionsBtn) { this.closeCaptionsMenu(); } }); // Hide scroll hint after scrolling const watchPage = document.getElementById('page-watch'); watchPage?.addEventListener('scroll', () => { if (watchPage.scrollTop > 50) { this.scrollHint?.classList.add('hidden'); } else { this.scrollHint?.classList.remove('hidden'); } }); } /** * Main entry point - play content * @param {Object} content - Movie or episode info * @param {string} streamUrl - Stream URL */ async play(content, streamUrl) { this.content = content; this.contentType = content.type; this.seriesInfo = content.seriesInfo || null; this.currentSeason = content.currentSeason || null; this.currentEpisode = content.currentEpisode || null; this.returnPage = content.type === 'movie' ? 'movies' : 'series'; // Reset state this.cancelNextEpisode(); // Navigate to watch page this.app.navigateTo('watch', true); // Scroll to top document.getElementById('page-watch')?.scrollTo(0, 0); // Update title bar this.titleEl.textContent = content.title || ''; this.subtitleEl.textContent = content.subtitle || ''; // Load video await this.loadVideo(streamUrl); // Populate details section this.renderDetails(); // Load recommended (movies) or episodes (series) if (content.type === 'movie') { this.episodesSection?.classList.add('hidden'); this.recommendedSection?.classList.remove('hidden'); await this.loadRecommended(content.sourceId, content.categoryId); } else { this.recommendedSection?.classList.add('hidden'); this.episodesSection?.classList.remove('hidden'); this.renderEpisodes(); } // Check favorite status await this.checkFavorite(); // Show overlay initially this.showOverlay(); } async loadVideo(url) { // Stop any existing playback this.stop(); // Show loading spinner this.showLoading(); // Get settings for proxy/transcode let settings = {}; try { settings = await API.settings.get(); } catch (e) { console.warn('Could not load settings'); } // Detect stream type const looksLikeHls = url.includes('.m3u8') || url.includes('m3u8'); const isRawTs = url.includes('.ts') && !url.includes('.m3u8'); const isDirectVideo = url.includes('.mp4') || url.includes('.mkv') || url.includes('.avi'); // Priority 1: Force Transcode - route through FFmpeg if (settings.forceTranscode) { console.log('[WatchPage] Force Transcode enabled'); const finalUrl = `/api/transcode?url=${encodeURIComponent(url)}`; this.video.src = finalUrl; this.video.play().catch(e => { if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e); }); this.setVolumeFromStorage(); return; } // Priority 2: Force Remux for raw TS streams if (settings.forceRemux && isRawTs) { console.log('[WatchPage] Force Remux enabled'); const finalUrl = `/api/remux?url=${encodeURIComponent(url)}`; this.video.src = finalUrl; this.video.play().catch(e => { if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e); }); this.setVolumeFromStorage(); return; } // Determine if proxy is needed const proxyRequiredDomains = ['pluto.tv']; const needsProxy = settings.forceProxy || proxyRequiredDomains.some(domain => url.includes(domain)); const finalUrl = needsProxy ? `/api/proxy/stream?url=${encodeURIComponent(url)}` : url; console.log('[WatchPage] Playing:', { url, needsProxy, looksLikeHls }); // Use HLS.js for HLS streams if (looksLikeHls && Hls.isSupported()) { this.hls = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60, startLevel: -1, enableWorker: true, }); this.hls.loadSource(finalUrl); this.hls.attachMedia(this.video); // Listen for subtitle track updates this.hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (event, data) => { console.log('[WatchPage] Subtitle tracks updated:', data.subtitleTracks); // Wait a moment for native text tracks to populate setTimeout(() => this.updateCaptionsTracks(), 100); }); this.hls.on(Hls.Events.SUBTITLE_TRACK_SWITCH, (event, data) => { console.log('[WatchPage] Subtitle track switched:', data); }); this.hls.on(Hls.Events.MANIFEST_PARSED, () => { this.video.play().catch(e => { if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e); }); }); this.hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) { console.error('[WatchPage] HLS fatal error:', data); // Try proxy on CORS error if (!needsProxy && (data.type === Hls.ErrorTypes.NETWORK_ERROR)) { console.log('[WatchPage] Retrying via proxy...'); this.hls.loadSource(`/api/proxy/stream?url=${encodeURIComponent(url)}`); this.hls.startLoad(); } else { this.hls.destroy(); } } }); } else { // Direct playback for mp4/mkv/avi this.video.src = finalUrl; this.video.play().catch(e => { if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e); }); } this.setVolumeFromStorage(); } setVolumeFromStorage() { const savedVolume = localStorage.getItem('nodecast-volume') || '80'; this.video.volume = parseInt(savedVolume) / 100; if (this.volumeSlider) this.volumeSlider.value = savedVolume; } stop() { if (this.hls) { this.hls.destroy(); this.hls = null; } if (this.video) { this.video.pause(); this.video.src = ''; this.video.load(); } } // === Playback Controls === togglePlay() { if (this.video.paused) { this.video.play().catch(console.error); } else { this.video.pause(); } } skip(seconds) { if (this.video) { this.video.currentTime = Math.max(0, Math.min(this.video.currentTime + seconds, this.video.duration || 0)); } } seek(percent) { if (this.video && this.video.duration) { this.video.currentTime = (percent / 100) * this.video.duration; } } toggleMute() { if (this.video) { this.video.muted = !this.video.muted; this.updateVolumeUI(); } } setVolume(value) { if (this.video) { this.video.volume = value / 100; this.video.muted = false; localStorage.setItem('nodecast-volume', value); this.updateVolumeUI(); } } toggleFullscreen() { const container = document.querySelector('.watch-video-section'); if (!document.fullscreenElement) { container?.requestFullscreen?.() || container?.webkitRequestFullscreen?.(); } else { document.exitFullscreen?.() || document.webkitExitFullscreen?.(); } } // === UI Updates === updateProgress() { if (!this.video || !this.video.duration) return; const percent = (this.video.currentTime / this.video.duration) * 100; this.progressSlider.value = percent; this.timeCurrent.textContent = this.formatTime(this.video.currentTime); } onMetadataLoaded() { // Duration is unreliable for transcoded streams, so we don't display it } onPlay() { // Update play/pause button icons this.playPauseBtn?.querySelector('.icon-play')?.classList.add('hidden'); this.playPauseBtn?.querySelector('.icon-pause')?.classList.remove('hidden'); this.centerPlayBtn?.classList.remove('show'); // Start overlay auto-hide this.startOverlayTimer(); } onPause() { this.playPauseBtn?.querySelector('.icon-play')?.classList.remove('hidden'); this.playPauseBtn?.querySelector('.icon-pause')?.classList.add('hidden'); this.centerPlayBtn?.classList.add('show'); // Keep overlay visible when paused this.showOverlay(); clearTimeout(this.overlayTimeout); } onEnded() { // For series, show next episode panel if (this.contentType === 'series' && this.seriesInfo) { const nextEp = this.getNextEpisode(); if (nextEp) { this.showNextEpisodePanel(nextEp); } } } onError(e) { // Only log actual fatal errors, not benign stream recovery events const error = this.video?.error; if (error && error.code) { console.error('[WatchPage] Video error:', error.code, error.message); } } updateVolumeUI() { const isMuted = this.video?.muted || this.video?.volume === 0; this.muteBtn?.querySelector('.icon-vol')?.classList.toggle('hidden', isMuted); this.muteBtn?.querySelector('.icon-muted')?.classList.toggle('hidden', !isMuted); } formatTime(seconds) { if (!seconds || isNaN(seconds)) return '0:00'; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) { return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } return `${m}:${s.toString().padStart(2, '0')}`; } // === Loading Spinner === showLoading() { this.loadingSpinner?.classList.add('show'); this.centerPlayBtn?.classList.remove('show'); } hideLoading() { this.loadingSpinner?.classList.remove('show'); } // === Captions === toggleCaptionsMenu() { if (this.captionsMenuOpen) { this.closeCaptionsMenu(); } else { this.updateCaptionsTracks(); this.captionsMenu?.classList.remove('hidden'); this.captionsMenuOpen = true; } } closeCaptionsMenu() { this.captionsMenu?.classList.add('hidden'); this.captionsMenuOpen = false; } updateCaptionsTracks() { if (!this.captionsList || !this.video) return; // Build list of available text tracks const tracks = this.video.textTracks; let html = ''; for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; if (track.kind === 'subtitles' || track.kind === 'captions') { const label = track.label || track.language || `Track ${i + 1}`; const isActive = track.mode === 'showing'; html += ``; } } // Check if any track is active, if not mark "Off" as active let anyActive = false; for (let i = 0; i < tracks.length; i++) { if (tracks[i].mode === 'showing') anyActive = true; } if (!anyActive) { html = html.replace('class="captions-option"', 'class="captions-option active"'); } this.captionsList.innerHTML = html; // Add click handlers this.captionsList.querySelectorAll('.captions-option').forEach(btn => { btn.addEventListener('click', () => this.selectCaptionTrack(parseInt(btn.dataset.index))); }); } selectCaptionTrack(index) { if (!this.video) return; const tracks = this.video.textTracks; // Disable all tracks for (let i = 0; i < tracks.length; i++) { tracks[i].mode = 'hidden'; } // Enable selected track if (index >= 0 && index < tracks.length) { tracks[index].mode = 'showing'; } // Update UI this.updateCaptionsTracks(); this.closeCaptionsMenu(); } // === Overlay Auto-Hide === showOverlay() { this.overlay?.classList.remove('hidden'); this.overlayVisible = true; this.startOverlayTimer(); } hideOverlay() { if (!this.video?.paused) { this.overlay?.classList.add('hidden'); this.overlayVisible = false; } } startOverlayTimer() { clearTimeout(this.overlayTimeout); this.overlayTimeout = setTimeout(() => this.hideOverlay(), 3000); } // === Keyboard Shortcuts === handleKeyboard(e) { // Only handle when watch page is active const watchPage = document.getElementById('page-watch'); if (!watchPage?.classList.contains('active')) return; // Don't handle if typing in input if (['INPUT', 'TEXTAREA'].includes(e.target.tagName)) return; switch (e.key) { case ' ': case 'k': e.preventDefault(); this.togglePlay(); break; case 'ArrowLeft': e.preventDefault(); this.skip(-10); this.showOverlay(); break; case 'ArrowRight': e.preventDefault(); this.skip(10); this.showOverlay(); break; case 'ArrowUp': e.preventDefault(); this.setVolume(Math.min(100, parseInt(this.volumeSlider.value) + 10)); this.volumeSlider.value = Math.min(100, parseInt(this.volumeSlider.value) + 10); this.showOverlay(); break; case 'ArrowDown': e.preventDefault(); this.setVolume(Math.max(0, parseInt(this.volumeSlider.value) - 10)); this.volumeSlider.value = Math.max(0, parseInt(this.volumeSlider.value) - 10); this.showOverlay(); break; case 'f': e.preventDefault(); this.toggleFullscreen(); break; case 'm': e.preventDefault(); this.toggleMute(); this.showOverlay(); break; case 'Escape': if (document.fullscreenElement) { document.exitFullscreen(); } else { this.goBack(); } break; } } // === Details Section === renderDetails() { if (!this.content) return; this.posterEl.src = this.content.poster || '/img/placeholder.png'; this.posterEl.alt = this.content.title || ''; this.contentTitleEl.textContent = this.content.title || ''; this.yearEl.textContent = this.content.year || ''; this.ratingEl.textContent = this.content.rating ? `★ ${this.content.rating}` : ''; this.descriptionEl.textContent = this.content.description || ''; // Update play button text if (this.playBtnText) { this.playBtnText.textContent = 'Play'; } } async checkFavorite() { if (!this.content) return; try { const itemId = this.contentType === 'movie' ? this.content.id : this.content.seriesId; const itemType = this.contentType === 'movie' ? 'movie' : 'series'; const result = await API.favorites.check(this.content.sourceId, itemId, itemType); this.isFavorite = result?.isFavorite || false; this.updateFavoriteUI(); } catch (e) { console.warn('Could not check favorite status'); } } async toggleFavorite() { if (!this.content) return; const itemId = this.contentType === 'movie' ? this.content.id : this.content.seriesId; const itemType = this.contentType === 'movie' ? 'movie' : 'series'; try { if (this.isFavorite) { await API.favorites.remove(this.content.sourceId, itemId, itemType); this.isFavorite = false; } else { await API.favorites.add(this.content.sourceId, itemId, itemType); this.isFavorite = true; } this.updateFavoriteUI(); } catch (e) { console.error('Error toggling favorite:', e); } } updateFavoriteUI() { const outlineIcon = this.favoriteBtn?.querySelector('.icon-fav-outline'); const filledIcon = this.favoriteBtn?.querySelector('.icon-fav-filled'); outlineIcon?.classList.toggle('hidden', this.isFavorite); filledIcon?.classList.toggle('hidden', !this.isFavorite); } scrollToVideo() { document.getElementById('page-watch')?.scrollTo({ top: 0, behavior: 'smooth' }); if (this.video?.paused) { this.video.play().catch(console.error); } } // === Recommended Movies === async loadRecommended(sourceId, categoryId) { if (!sourceId || !categoryId) { this.recommendedSection?.classList.add('hidden'); return; } try { const movies = await API.proxy.xtream.vodStreams(sourceId, categoryId); if (!movies || movies.length === 0) { this.recommendedSection?.classList.add('hidden'); return; } // Filter out current movie, take first 12 const filtered = movies .filter(m => m.stream_id !== this.content?.id) .slice(0, 12); this.renderRecommendedGrid(filtered, sourceId); } catch (e) { console.error('Error loading recommended:', e); this.recommendedSection?.classList.add('hidden'); } } renderRecommendedGrid(movies, sourceId) { if (!this.recommendedGrid) return; this.recommendedGrid.innerHTML = movies.map(movie => ` `).join(''); // Click handlers this.recommendedGrid.querySelectorAll('.watch-recommended-card').forEach(card => { card.addEventListener('click', () => this.playRecommendedMovie(card.dataset.id, parseInt(card.dataset.source))); }); } async playRecommendedMovie(streamId, sourceId) { try { // Fetch movie details const movies = await API.proxy.xtream.vodStreams(sourceId); const movie = movies?.find(m => m.stream_id == streamId); if (!movie) return; const container = movie.container_extension || 'mp4'; const result = await API.proxy.xtream.getStreamUrl(sourceId, streamId, 'movie', container); if (result?.url) { this.play({ type: 'movie', id: movie.stream_id, title: movie.name, poster: movie.stream_icon || movie.cover, description: movie.plot || '', year: movie.year, rating: movie.rating, sourceId: sourceId, categoryId: movie.category_id }, result.url); } } catch (e) { console.error('Error playing recommended movie:', e); } } // === Series Episodes === renderEpisodes() { if (!this.seriesInfo?.episodes || !this.seasonsContainer) return; const seasons = Object.keys(this.seriesInfo.episodes).sort((a, b) => parseInt(a) - parseInt(b)); this.seasonsContainer.innerHTML = seasons.map(seasonNum => { const episodes = this.seriesInfo.episodes[seasonNum]; const isCurrentSeason = parseInt(seasonNum) === parseInt(this.currentSeason); return `
Season ${seasonNum} ${episodes.length} episodes
${episodes.map(ep => { const isActive = parseInt(seasonNum) === parseInt(this.currentSeason) && parseInt(ep.episode_num) === parseInt(this.currentEpisode); return `
E${ep.episode_num} ${ep.title || `Episode ${ep.episode_num}`} ${ep.duration || ''}
`; }).join('')}
`; }).join(''); // Season header toggle this.seasonsContainer.querySelectorAll('.watch-season-header').forEach(header => { header.addEventListener('click', () => { header.classList.toggle('collapsed'); }); }); // Episode click handlers this.seasonsContainer.querySelectorAll('.watch-episode-item').forEach(ep => { ep.addEventListener('click', () => this.playEpisodeFromList(ep)); }); } async playEpisodeFromList(episodeEl) { const episodeId = episodeEl.dataset.episodeId; const seasonNum = episodeEl.dataset.season; const episodeNum = episodeEl.dataset.episode; const container = episodeEl.dataset.container || 'mp4'; try { const result = await API.proxy.xtream.getStreamUrl(this.content.sourceId, episodeId, 'series', container); if (result?.url) { const episodeTitle = episodeEl.querySelector('.watch-episode-title')?.textContent || `Episode ${episodeNum}`; this.play({ type: 'series', id: episodeId, title: this.content.title, subtitle: `S${seasonNum} E${episodeNum} - ${episodeTitle}`, poster: this.content.poster, description: this.content.description, year: this.content.year, rating: this.content.rating, sourceId: this.content.sourceId, seriesId: this.content.seriesId, seriesInfo: this.seriesInfo, currentSeason: seasonNum, currentEpisode: episodeNum }, result.url); } } catch (e) { console.error('Error playing episode:', e); } } // === Next Episode === getNextEpisode() { if (!this.seriesInfo?.episodes || !this.currentSeason || !this.currentEpisode) return null; const seasons = Object.keys(this.seriesInfo.episodes).sort((a, b) => parseInt(a) - parseInt(b)); const currentSeasonEpisodes = this.seriesInfo.episodes[this.currentSeason] || []; // Find next episode in current season const currentEpIndex = currentSeasonEpisodes.findIndex(ep => parseInt(ep.episode_num) === parseInt(this.currentEpisode) ); if (currentEpIndex >= 0 && currentEpIndex < currentSeasonEpisodes.length - 1) { return { ...currentSeasonEpisodes[currentEpIndex + 1], seasonNum: this.currentSeason }; } // Try next season const currentSeasonIndex = seasons.indexOf(String(this.currentSeason)); if (currentSeasonIndex >= 0 && currentSeasonIndex < seasons.length - 1) { const nextSeason = seasons[currentSeasonIndex + 1]; const nextSeasonEpisodes = this.seriesInfo.episodes[nextSeason]; if (nextSeasonEpisodes?.length > 0) { return { ...nextSeasonEpisodes[0], seasonNum: nextSeason }; } } return null; } showNextEpisodePanel(nextEp) { if (!this.nextEpisodePanel) return; this.nextEpisodeTitle.textContent = `S${nextEp.seasonNum} E${nextEp.episode_num} - ${nextEp.title || `Episode ${nextEp.episode_num}`}`; this.nextEpisodePanel.classList.remove('hidden'); this.nextEpisodePanel.nextEpisodeData = nextEp; // Start countdown this.nextEpisodeCountdown = 10; this.nextCountdown.textContent = this.nextEpisodeCountdown; this.nextEpisodeInterval = setInterval(() => { this.nextEpisodeCountdown--; this.nextCountdown.textContent = this.nextEpisodeCountdown; if (this.nextEpisodeCountdown <= 0) { this.playNextEpisode(); } }, 1000); } async playNextEpisode() { this.cancelNextEpisode(); const nextEp = this.nextEpisodePanel?.nextEpisodeData; if (!nextEp) return; try { const container = nextEp.container_extension || 'mp4'; const result = await API.proxy.xtream.getStreamUrl(this.content.sourceId, nextEp.id, 'series', container); if (result?.url) { this.play({ type: 'series', id: nextEp.id, title: this.content.title, subtitle: `S${nextEp.seasonNum} E${nextEp.episode_num} - ${nextEp.title || `Episode ${nextEp.episode_num}`}`, poster: this.content.poster, description: this.content.description, year: this.content.year, rating: this.content.rating, sourceId: this.content.sourceId, seriesId: this.content.seriesId, seriesInfo: this.seriesInfo, currentSeason: nextEp.seasonNum, currentEpisode: nextEp.episode_num }, result.url); } } catch (e) { console.error('Error playing next episode:', e); } } cancelNextEpisode() { clearInterval(this.nextEpisodeInterval); this.nextEpisodePanel?.classList.add('hidden'); if (this.nextEpisodePanel) { this.nextEpisodePanel.nextEpisodeData = null; } } // === Navigation === goBack() { this.stop(); this.cancelNextEpisode(); // Navigate to the page we came from (stored in returnPage) // We don't use history.back() because we used replaceHistory when navigating here this.app.navigateTo(this.returnPage || 'movies'); } show() { // Called when page becomes visible } hide() { // Called when page becomes hidden this.stop(); this.cancelNextEpisode(); } } window.WatchPage = WatchPage;