Files
NotPlexApp/renderer.js

849 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
window.addEventListener('DOMContentLoaded', () => {
let cancelAutoAdvance = false; // Global flag for auto-advance cancellation
let syncSocket = null; // WebSocket for video sync
let isSyncing = false; // Flag to prevent infinite loops during sync
const authPage = document.getElementById('auth-page');
const loginBtn = document.getElementById('login-btn');
const registerBtn = document.getElementById('register-btn');
const authError = document.getElementById('auth-error');
const moviesListEl = document.getElementById('movies-list');
const showsListEl = document.getElementById('shows-list');
const overlay = document.getElementById('overlay');
const movieDetailsEl = document.getElementById('movie-details');
const closeOverlayBtn = document.getElementById('close-overlay');
const moviesTabBtn = document.getElementById('movies-tab');
const showsTabBtn = document.getElementById('shows-tab');
const scanBtn = document.getElementById('scan-btn');
// Listen for token expiration events from preload.
window.addEventListener("tokenExpired", () => {
console.warn("Access token expired. Redirecting to login screen.");
authPage.style.display = 'flex';
});
// Modified authentication check: hide the login overlay if a token is loaded.
function checkAuth() {
if (window.api.getToken()) {
authPage.style.display = 'none';
} else {
authPage.style.display = 'flex';
}
}
loginBtn.addEventListener('click', () => {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
window.api.login(username, password).then(data => {
if(data.access_token) {
authPage.style.display = 'none';
loadMovies();
} else {
authError.textContent = data.error || 'Login failed';
}
}).catch(err => {
authError.textContent = 'Login error';
console.error(err);
});
});
registerBtn.addEventListener('click', () => {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
window.api.register(username, password).then(data => {
if(data.success) {
authError.textContent = 'Registration successful. Please log in.';
} else {
authError.textContent = data.error || 'Registration failed';
}
}).catch(err => {
authError.textContent = 'Registration error';
console.error(err);
});
});
// Helper to boost video volume using the Web Audio API
function boostVideoVolume(video) {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
const source = audioContext.createMediaElementSource(video);
const gainNode = audioContext.createGain();
gainNode.gain.value = 5; // 200% volume boost
source.connect(gainNode);
gainNode.connect(audioContext.destination);
} catch (err) {
console.error('Error boosting video volume:', err);
}
}
// New: transcodeAudioStream uses ffmpeg.wasm to re-encode the audio
async function transcodeAudioStream(videoUrl) {
// Ensure ffmpeg is loaded only once
if (!window.ffmpegInstance) {
const { createFFmpeg, fetchFile } = FFmpeg;
window.ffmpegInstance = createFFmpeg({ log: true });
await window.ffmpegInstance.load();
}
const ffmpeg = window.ffmpegInstance;
try {
// Fetch the original video file as an ArrayBuffer
const response = await fetch(videoUrl);
const data = await response.arrayBuffer();
ffmpeg.FS('writeFile', 'input.mp4', new Uint8Array(data));
// Run ffmpeg command to copy the video stream and transcode the audio to AAC
await ffmpeg.run('-i', 'input.mp4', '-c:v', 'copy', '-c:a', 'aac', 'output.mp4');
const outputData = ffmpeg.FS('readFile', 'output.mp4');
// Clean up the virtual FS
ffmpeg.FS('unlink', 'input.mp4');
ffmpeg.FS('unlink', 'output.mp4');
const blob = new Blob([outputData.buffer], { type: 'video/mp4' });
const transcodedUrl = URL.createObjectURL(blob);
return transcodedUrl;
} catch (error) {
console.error("Transcoding error:", error);
throw error;
}
}
// New: try direct play, detect “no audio” or error, then remux on demand
async function safePlayVideo(videoElement, videoUrl, subtitlesUrl = null) {
// 1) wire up subtitles (same as you had)
if (subtitlesUrl) {
let track = videoElement.querySelector('track');
if (!track) {
track = document.createElement('track');
track.kind = 'subtitles';
track.label = 'English';
track.srclang = 'en';
videoElement.appendChild(track);
}
track.src = subtitlesUrl;
track.default = true;
}
// 2) helper that tries to play + checks for audioTracks.length
const tryPlay = url => new Promise((resolve, reject) => {
videoElement.src = url;
videoElement.load();
const onErr = () => cleanup() || reject();
const onPlay = () => {
setTimeout(() => {
const hasAudio = videoElement.audioTracks
? videoElement.audioTracks.length > 0
: true; // assume “ok” if browser doesnt support audioTracks
cleanup();
hasAudio ? resolve() : reject();
}, 200);
};
function cleanup() {
videoElement.removeEventListener('error', onErr);
videoElement.removeEventListener('play', onPlay);
return true;
}
videoElement.addEventListener('error', onErr);
videoElement.addEventListener('play', onPlay);
videoElement.play().catch(onErr);
});
try {
// first attempt: raw MKV
await tryPlay(videoUrl);
} catch (_) {
console.warn('Direct play failed or no audio → remuxing…');
// insert the indicator right above the video
const indicator = document.createElement('div');
indicator.classList.add('remuxing-indicator');
indicator.textContent = 'Remuxing...';
videoElement.parentNode.insertBefore(indicator, videoElement);
// do the heavy FFmpeg step
const mp4url = await transcodeAudioStream(videoUrl);
await tryPlay(mp4url);
// remove the banner
indicator.remove();
}
}
// Add error handling for video playback
function playVideo(videoElement, videoUrl, subtitlesUrl = null) {
try {
const mediaType = videoElement.dataset.mediaType;
const mediaId = videoElement.dataset.mediaId;
window.api.getProgress(mediaType, mediaId)
.then(data => {
// Determine progress; default to 0 if none found.
const progress = (data.last_position && data.last_position > 0) ? data.last_position : 0;
// Listen for metadata load to seek to the progress point.
videoElement.addEventListener('loadedmetadata', function setProgress() {
if (progress > 0) {
videoElement.currentTime = progress;
}
videoElement.removeEventListener('loadedmetadata', setProgress);
});
safePlayVideo(videoElement, videoUrl, subtitlesUrl)
.then(() => {
// Define helper functions to start and stop the progress timer.
function startProgressTimer() {
if (!videoElement.progressTimer) {
videoElement.progressTimer = setInterval(() => {
window.api.saveProgress(mediaType, mediaId, videoElement.currentTime)
.catch(err => console.error("Error saving progress:", err));
}, 60000); // 5 minutes in ms
}
}
function stopProgressTimer() {
if (videoElement.progressTimer) {
clearInterval(videoElement.progressTimer);
videoElement.progressTimer = null;
}
}
videoElement.addEventListener('play', startProgressTimer);
videoElement.addEventListener('pause', stopProgressTimer);
videoElement.addEventListener('ended', stopProgressTimer);
if (!videoElement.paused) {
startProgressTimer();
}
})
.catch(err => console.error('Playback failed even after remux:', err));
})
.catch(err => {
console.warn('Failed to fetch progress:', err);
// Continue with playback even if fetching progress failed.
safePlayVideo(videoElement, videoUrl, subtitlesUrl)
.then(() => {
function startProgressTimer() {
if (!videoElement.progressTimer) {
videoElement.progressTimer = setInterval(() => {
window.api.saveProgress(mediaType, mediaId, videoElement.currentTime)
.catch(err => console.error("Error saving progress:", err));
}, 300000);
}
}
function stopProgressTimer() {
if (videoElement.progressTimer) {
clearInterval(videoElement.progressTimer);
videoElement.progressTimer = null;
}
}
videoElement.addEventListener('play', startProgressTimer);
videoElement.addEventListener('pause', stopProgressTimer);
videoElement.addEventListener('ended', stopProgressTimer);
if (!videoElement.paused) {
startProgressTimer();
}
})
.catch(err => console.error('Playback failed even after remux:', err));
});
} catch (err) {
console.error('Unexpected playback error:', err);
alert('An unexpected error occurred. Please try again.');
}
}
// Helper to switch active tab and content
function setActiveTab(tab) {
if (tab === 'movies') {
moviesTabBtn.classList.add('active');
showsTabBtn.classList.remove('active');
moviesListEl.style.display = ''; // use CSS default (i.e. .section-list)
showsListEl.style.display = 'none';
} else {
moviesTabBtn.classList.remove('active');
showsTabBtn.classList.add('active');
moviesListEl.style.display = 'none';
showsListEl.style.display = ''; // use CSS default (i.e. .section-list)
}
}
moviesTabBtn.addEventListener('click', () => {
setActiveTab('movies');
});
showsTabBtn.addEventListener('click', () => {
setActiveTab('shows');
if (!showsListEl.hasChildNodes()) loadShows();
});
// Load and display movie list
function loadMovies() {
// New: Load and display in-progress movies
loadInProgressMovies();
window.api.getMovies().then(movies => {
// --- Newest Movies Section ---
const sectionNewest = document.createElement('section');
const headerNewest = document.createElement('h2');
headerNewest.textContent = 'Newest Movies';
sectionNewest.appendChild(headerNewest);
const gridNewest = document.createElement('div');
gridNewest.classList.add('content-row');
// sort by created_at descending (newest first)
const moviesByNewest = [...movies].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
moviesByNewest.forEach(movie => {
const item = document.createElement('div');
item.classList.add('movie-item');
item.innerHTML = `
<img src="${movie.poster !== 'N/A' ? movie.poster : ''}" alt="${movie.title}" />
<span>${movie.title}</span>
`;
item.addEventListener('click', () => {
window.api.getMovieDetails(movie.id).then(movieDetails => {
movieDetailsEl.innerHTML = `
<h2>${movieDetails.title}</h2>
<p>${movieDetails.plot}</p>
<p><strong>Year:</strong> ${movieDetails.year}</p>
<p><strong>Released:</strong> ${movieDetails.released}</p>
<video id="movie-video" controls autoplay
data-media-id="${movieDetails.id}"
data-media-type="movie"></video>
`;
overlay.style.display = 'flex';
const movieVideo = document.getElementById('movie-video');
boostVideoVolume(movieVideo);
const streamUrl = window.api.getStream(movieDetails.id);
const subtitlesUrl = window.api.getSubtitles(movieDetails.id);
playVideo(movieVideo, streamUrl, subtitlesUrl);
});
});
gridNewest.appendChild(item);
});
// wrap row + arrows
const scrollWrap = document.createElement('div');
scrollWrap.classList.add('scroll-container');
// left arrow
const leftBtn = document.createElement('button');
leftBtn.classList.add('scroll-arrow','left');
leftBtn.innerHTML = '&#9664;';
leftBtn.addEventListener('click', ()=> {
gridNewest.scrollBy({ left: -300, behavior: 'smooth' });
});
// right arrow
const rightBtn = document.createElement('button');
rightBtn.classList.add('scroll-arrow','right');
rightBtn.innerHTML = '&#9654;';
rightBtn.addEventListener('click', ()=> {
gridNewest.scrollBy({ left: 300, behavior: 'smooth' });
});
scrollWrap.append(leftBtn, gridNewest, rightBtn);
sectionNewest.appendChild(scrollWrap);
moviesListEl.appendChild(sectionNewest);
// --- Alphabetical Movies Section ---
const sectionAlpha = document.createElement('section');
const headerAlpha = document.createElement('h2');
headerAlpha.textContent = 'All Movies (AZ)';
sectionAlpha.appendChild(headerAlpha);
const gridAlpha = document.createElement('div');
gridAlpha.classList.add('content-list');
// sort by title ascending
const moviesAlpha = [...movies].sort((a, b) => a.title.localeCompare(b.title));
moviesAlpha.forEach(movie => {
const item = document.createElement('div');
item.classList.add('movie-item');
item.innerHTML = `
<img src="${movie.poster !== 'N/A' ? movie.poster : ''}" alt="${movie.title}" />
<span>${movie.title}</span>
`;
item.addEventListener('click', () => {
window.api.getMovieDetails(movie.id).then(movieDetails => {
movieDetailsEl.innerHTML = `
<h2>${movieDetails.title}</h2>
<p>${movieDetails.plot}</p>
<p><strong>Year:</strong> ${movieDetails.year}</p>
<p><strong>Released:</strong> ${movieDetails.released}</p>
<video id="movie-video" controls autoplay
data-media-id="${movieDetails.id}"
data-media-type="movie"></video>
`;
overlay.style.display = 'flex';
const movieVideo = document.getElementById('movie-video');
boostVideoVolume(movieVideo);
const streamUrl = window.api.getStream(movieDetails.id);
const subtitlesUrl = window.api.getSubtitles(movieDetails.id);
playVideo(movieVideo, streamUrl, subtitlesUrl);
});
});
gridAlpha.appendChild(item);
});
sectionAlpha.appendChild(gridAlpha);
moviesListEl.appendChild(sectionAlpha);
});
}
// New: Function to load currently in-progress movies
function loadInProgressMovies() {
window.api.getInProgress().then(data => {
const movieProgress = data.movies || [];
if (movieProgress.length > 0) {
const sectionInProgress = document.createElement('section');
const headerInProgress = document.createElement('h2');
headerInProgress.textContent = 'Continue Watching';
sectionInProgress.appendChild(headerInProgress);
const gridInProgress = document.createElement('div');
gridInProgress.classList.add('content-row');
movieProgress.forEach(item => {
// item.media_id holds the movie id.
window.api.getMovieDetails(item.media_id).then(movieDetails => {
const movieItem = document.createElement('div');
movieItem.classList.add('movie-item');
movieItem.innerHTML = `
<img src="${movieDetails.poster !== 'N/A' ? movieDetails.poster : ''}" alt="${movieDetails.title}" />
<span>${movieDetails.title}</span>
`;
movieItem.addEventListener('click', () => {
movieDetailsEl.innerHTML = `
<h2>${movieDetails.title}</h2>
<p>${movieDetails.plot}</p>
<p><strong>Year:</strong> ${movieDetails.year}</p>
<p><strong>Released:</strong> ${movieDetails.released}</p>
<video id="movie-video" controls autoplay
data-media-id="${movieDetails.id}"
data-media-type="movie"></video>
`;
overlay.style.display = 'flex';
const movieVideo = document.getElementById('movie-video');
boostVideoVolume(movieVideo);
const streamUrl = window.api.getStream(movieDetails.id);
const subtitlesUrl = window.api.getSubtitles(movieDetails.id);
playVideo(movieVideo, streamUrl, subtitlesUrl);
});
gridInProgress.appendChild(movieItem);
}).catch(err => console.error("Failed to load movie details for progress item:", err));
});
sectionInProgress.appendChild(gridInProgress);
moviesListEl.insertBefore(sectionInProgress, moviesListEl.firstChild);
}
}).catch(err => console.error('Error loading in progress movies:', err));
}
// Load and display TV shows list
async function loadShows() {
// New: Load and display in-progress episodes for TV shows
loadInProgressShows();
const shows = await window.api.getShows();
showsListEl.innerHTML = '';
// --- Newest TV Shows Section (by latest episode added) ---
const sectionNewest = document.createElement('section');
const headerNewest = document.createElement('h2');
headerNewest.textContent = 'Newest TV Shows (by latest episode)';
sectionNewest.appendChild(headerNewest);
const gridNewest = document.createElement('div');
gridNewest.classList.add('content-row');
// Fetch seasons & episodes for each show to find the most recent episode date
const showsWithLatestEp = await Promise.all(shows.map(async show => {
let seasons = await window.api.getShowSeasons(show.id);
if (!Array.isArray(seasons)) seasons = Object.values(seasons);
const episodeLists = await Promise.all(
seasons.map(season => window.api.getSeasonEpisodes(show.id, season))
);
const allEpisodes = episodeLists.flat();
const latestDate = allEpisodes.reduce((max, ep) => {
const dt = new Date(ep.created_at);
return dt > max ? dt : max;
}, new Date(0));
return { show, latestDate };
}));
// Sort shows by their latest episode date (newest first)
const showsByNewest = showsWithLatestEp
.sort((a, b) => b.latestDate - a.latestDate)
.map(item => item.show);
showsByNewest.forEach(show => {
const item = document.createElement('div');
item.classList.add('movie-item');
item.innerHTML = `
<img src="${show.poster || ''}" alt="${show.name}" />
<span>${show.name}</span>
`;
item.addEventListener('click', () => {
window.api.getShowDetails(show.id).then(showDetails => {
const showHTML = `
<h2>${showDetails.name}</h2>
<img src="${showDetails.poster}" alt="${showDetails.name}" style="max-width:200px;"/>
<p><strong>Rating:</strong> ${showDetails.rating}</p>
<p><strong>Genres:</strong> ${showDetails.genres}</p>
<p>${showDetails.summary}</p>
<h3>Seasons:</h3>
`;
window.api.getShowSeasons(show.id).then(seasons => {
if (!Array.isArray(seasons)) seasons = Object.values(seasons);
const seasonsHTML = seasons
.map(s => `<button class="season-btn" data-show-id="${show.id}" data-season="${s}">Season ${s}</button>`)
.join('');
movieDetailsEl.innerHTML = showHTML + seasonsHTML;
overlay.style.display = 'flex';
overlay.querySelectorAll('.season-btn').forEach(btn =>
btn.addEventListener('click', seasonBtnHandler)
);
});
});
});
gridNewest.appendChild(item);
});
sectionNewest.appendChild(gridNewest);
showsListEl.appendChild(sectionNewest);
// --- Alphabetical TV Shows Section ---
const sectionAlpha = document.createElement('section');
const headerAlpha = document.createElement('h2');
headerAlpha.textContent = 'All TV Shows (AZ)';
sectionAlpha.appendChild(headerAlpha);
const gridAlpha = document.createElement('div');
gridAlpha.classList.add('content-list');
const showsAlpha = [...shows].sort((a, b) => a.name.localeCompare(b.name));
showsAlpha.forEach(show => {
const item = document.createElement('div');
item.classList.add('movie-item');
item.innerHTML = `
<img src="${show.poster || ''}" alt="${show.name}" />
<span>${show.name}</span>
`;
item.addEventListener('click', () => {
window.api.getShowDetails(show.id).then(showDetails => {
const showHTML = `
<h2>${showDetails.name}</h2>
<img src="${showDetails.poster}" alt="${showDetails.name}" style="max-width:200px;"/>
<p><strong>Rating:</strong> ${showDetails.rating}</p>
<p><strong>Genres:</strong> ${showDetails.genres}</p>
<p>${showDetails.summary}</p>
<h3>Seasons:</h3>
`;
window.api.getShowSeasons(show.id).then(seasons => {
if (!Array.isArray(seasons)) seasons = Object.values(seasons);
const seasonsHTML = seasons
.map(s => `<button class="season-btn" data-show-id="${show.id}" data-season="${s}">Season ${s}</button>`)
.join('');
movieDetailsEl.innerHTML = showHTML + seasonsHTML;
overlay.style.display = 'flex';
overlay.querySelectorAll('.season-btn').forEach(btn =>
btn.addEventListener('click', seasonBtnHandler)
);
});
});
});
gridAlpha.appendChild(item);
});
sectionAlpha.appendChild(gridAlpha);
showsListEl.appendChild(sectionAlpha);
}
// Updated function to stream the episode without calling getEpisodeDetails
function loadInProgressShows() {
window.api.getInProgress().then(data => {
const episodesProgress = data.episodes || [];
if (episodesProgress.length > 0) {
const sectionInProgress = document.createElement('section');
const headerInProgress = document.createElement('h2');
headerInProgress.textContent = 'Continue Watching';
sectionInProgress.appendChild(headerInProgress);
const gridInProgress = document.createElement('div');
gridInProgress.classList.add('content-row');
episodesProgress.forEach(item => {
window.api.getShowByEpisode(item.media_id)
.then(showDetails => {
const episodeItem = document.createElement('div');
episodeItem.classList.add('movie-item');
episodeItem.innerHTML = `
<img src="${showDetails.poster || ''}" alt="${showDetails.name}" />
<span>${showDetails.name}</span>
`;
episodeItem.addEventListener('click', () => {
movieDetailsEl.innerHTML = `
<h2>Episode ${item.media_id}</h2>
<video id="episode-video" controls autoplay
data-media-id="${item.media_id}"
data-media-type="episode"></video>
`;
overlay.style.display = 'flex';
const videoElement = document.getElementById('episode-video');
const episodeStreamUrl = window.api.getEpisodeStream(item.media_id);
playVideo(videoElement, episodeStreamUrl);
});
gridInProgress.appendChild(episodeItem);
})
.catch(err => {
console.error("Failed to fetch show by episode details:", err);
});
});
sectionInProgress.appendChild(gridInProgress);
showsListEl.insertBefore(sectionInProgress, showsListEl.firstChild);
}
}).catch(err => console.error('Error loading in progress shows:', err));
}
scanBtn.addEventListener('click', () => {
scanBtn.disabled = true; // Disable the button to prevent multiple clicks
scanBtn.textContent = 'Scanning...';
window.api.scanForNewFiles().then(response => {
if (response.success) {
alert('Scan completed successfully!');
loadMovies();
loadShows();
} else {
alert('Scan failed: ' + (response.error || 'Unknown error'));
}
}).catch(err => {
console.error('Error during scan:', err);
alert('An error occurred while scanning for new files.');
}).finally(() => {
scanBtn.disabled = false;
scanBtn.textContent = 'Scan for New Files';
});
});
// Initial auth check; hide login overlay if token exists and load movies.
checkAuth();
if (window.api.getToken()) {
loadMovies();
}
closeOverlayBtn.addEventListener('click', () => {
cancelAutoAdvance = true;
const video = overlay.querySelector('video');
if (video) {
video.pause();
video.currentTime = 0;
// Stop the progress timer if it's running
if (video.progressTimer) {
clearInterval(video.progressTimer);
video.progressTimer = null;
}
video.removeAttribute('src');
video.load();
}
overlay.style.display = 'none';
});
// Add handler for season buttons
function seasonBtnHandler(event) {
const btn = event.currentTarget;
const showId = btn.dataset.showId;
const season = btn.dataset.season;
// Clear previous details and show season header
movieDetailsEl.innerHTML = `<h2>Season ${season} Episodes</h2>`;
window.api.getSeasonEpisodes(showId, season)
.then(episodes => {
if (episodes && episodes.length) {
const episodesContainer = document.createElement('div');
episodesContainer.classList.add('episodes-list');
episodes.forEach(ep => {
const epBtn = document.createElement('button');
epBtn.classList.add('episode-btn');
epBtn.textContent = ep.title || `Episode ${ep.id}`;
epBtn.dataset.episodeId = ep.id;
epBtn.addEventListener('click', () => {
// Display selected episode video in the overlay
movieDetailsEl.innerHTML = `
<h2>${ep.title}</h2>
<video id="episode-video" controls autoplay
data-media-id="${ep.id}"
data-media-type="episode"></video>
`;
const videoElement = document.getElementById('episode-video');
const episodeStreamUrl = window.api.getEpisodeStream(ep.id);
playVideo(videoElement, episodeStreamUrl);
});
episodesContainer.appendChild(epBtn);
});
movieDetailsEl.appendChild(episodesContainer);
} else {
movieDetailsEl.innerHTML += '<p>No episodes found for this season.</p>';
}
})
.catch(err => {
console.error('Error fetching episodes:', err);
movieDetailsEl.innerHTML += '<p>Error loading episodes.</p>';
});
}
function connectToSyncSession(sessionId, mediaId, mediaType, videoElement = null) {
if (syncSocket) {
syncSocket.close();
}
// Include media_id and media_type as query parameters in the WebSocket URL
syncSocket = new WebSocket(`ws://bbrunson.com:8495/ws/sync/${sessionId}?media_id=${mediaId}&media_type=${mediaType}`);
syncSocket.onopen = () => {
console.log("Connected to sync session:", sessionId);
};
syncSocket.onmessage = async (event) => {
if (isSyncing) return; // Prevent loops caused by local updates
const message = JSON.parse(event.data);
// Ensure the media_id and media_type match the current video
if (message.media_id !== mediaId || message.media_type !== mediaType) {
console.warn("Received sync message for a different media_id or media_type. Ignoring.");
return;
}
const data = JSON.parse(message.data);
// If no video element is provided, fetch video details and start playback
if (!videoElement) {
try {
let videoDetails;
if (mediaType === "movie") {
videoDetails = await window.api.getMovieDetails(mediaId); // Fetch movie details
} else if (mediaType === "episode") {
videoDetails = await window.api.getEpisodeDetails(mediaId); // Fetch episode details
} else {
throw new Error("Unknown media type");
}
movieDetailsEl.innerHTML = `
<h2>${videoDetails.title}</h2>
<p>${videoDetails.plot}</p>
<p><strong>Year:</strong> ${videoDetails.year}</p>
<p><strong>Released:</strong> ${videoDetails.released}</p>
<video id="movie-video" controls autoplay data-media-id="${videoDetails.id}" data-media-type="${mediaType}"></video>
`;
overlay.style.display = 'flex';
videoElement = document.getElementById('movie-video');
boostVideoVolume(videoElement);
const streamUrl = mediaType === "movie"
? window.api.getStream(videoDetails.id)
: window.api.getEpisodeStream(videoDetails.id);
playVideo(videoElement, streamUrl);
} catch (error) {
console.error("Error fetching video details:", error);
alert("Failed to load video for the sync session.");
return;
}
}
const currentTime = videoElement.currentTime;
const timeDifference = Math.abs(currentTime - data.currentTime);
if (data.type === "play") {
if (timeDifference > 5) {
videoElement.currentTime = data.currentTime;
}
videoElement.play();
} else if (data.type === "pause") {
if (timeDifference > 5) {
videoElement.currentTime = data.currentTime;
}
videoElement.pause();
} else if (data.type === "seek") {
if (timeDifference > 5) {
videoElement.currentTime = data.currentTime;
}
}
};
syncSocket.onclose = () => {
console.log("Disconnected from sync session:", sessionId);
};
syncSocket.onerror = (error) => {
console.error("WebSocket error:", error);
};
// Sync video events with the server
if (videoElement) {
videoElement.addEventListener("play", () => {
if (syncSocket.readyState === WebSocket.OPEN) {
isSyncing = true;
syncSocket.send(JSON.stringify({ type: "play", currentTime: videoElement.currentTime }));
isSyncing = false;
}
});
videoElement.addEventListener("pause", () => {
if (syncSocket.readyState === WebSocket.OPEN) {
isSyncing = true;
syncSocket.send(JSON.stringify({ type: "pause", currentTime: videoElement.currentTime }));
isSyncing = false;
}
});
videoElement.addEventListener("seeked", () => {
if (syncSocket.readyState === WebSocket.OPEN) {
isSyncing = true;
syncSocket.send(JSON.stringify({ type: "seek", currentTime: videoElement.currentTime }));
isSyncing = false;
}
});
}
}
document.getElementById("join-sync-btn").addEventListener("click", async () => {
const sessionId = document.getElementById("sync-session-id").value.trim();
if (!sessionId) {
alert("Please enter a valid session ID.");
return;
}
// Fetch session details from the server
let mediaId, mediaType;
try {
const sessionDetails = await window.api.getSessionDetails(sessionId);
mediaId = sessionDetails.media_id;
mediaType = sessionDetails.media_type;
} catch (error) {
console.error("Error fetching session details:", error);
alert("Failed to retrieve session details. Please try again.");
return;
}
if (!mediaId || !mediaType) {
alert("Media ID or type is missing. Unable to join the sync session.");
return;
}
const videoElement = document.querySelector("video");
connectToSyncSession(sessionId, mediaId, mediaType, videoElement);
});
document.getElementById("overlay-join-sync-btn").addEventListener("click", () => {
const sessionId = document.getElementById("overlay-sync-session-id").value.trim();
if (!sessionId) {
alert("Please enter a valid session ID.");
return;
}
// Automatically retrieve the mediaId from the currently playing video in the overlay
const videoElement = document.querySelector("#overlay video");
if (!videoElement || !videoElement.dataset.mediaId) {
alert("No video is currently playing or media ID is missing.");
return;
}
const mediaId = videoElement.dataset.mediaId; // Retrieve mediaId from the video element
connectToSyncSession(sessionId, mediaId, "movie", videoElement);
});
});