continue watching section, recently added section, new endpoints in collab with server, scan for new files, and more

This commit is contained in:
Brandon4466
2025-05-29 12:58:00 -07:00
parent 3e80923494
commit f8f04c19cf
6 changed files with 1201 additions and 37 deletions

View File

@@ -1,30 +1,849 @@
window.addEventListener('DOMContentLoaded', () => {
const listEl = document.getElementById('movies-list');
const detailsEl = document.getElementById('movie-details');
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');
// Load and display movie list
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 => {
movies.forEach(movie => {
// --- 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', () => loadDetails(movie.id));
listEl.appendChild(item);
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);
});
});
// Fetch and display details + stream
function loadDetails(id) {
window.api.getMovieDetails(id).then(movie => {
detailsEl.innerHTML = `
<h2>${movie.title}</h2>
<p><strong>Year:</strong> ${movie.year}</p>
<p><strong>Released:</strong> ${movie.released}</p>
<video controls src="${window.api.getStreamUrl(movie.id)}"></video>
// 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);
});
});