document.addEventListener("DOMContentLoaded", () => { const playerWrapper = document.getElementById("player-wrapper"); const preparationOverlay = document.getElementById("preparation-overlay"); const preparationStatus = document.getElementById("preparation-status"); const preparationProgressBar = document.getElementById("preparation-progress-bar"); const reloadButton = document.getElementById("reload-button"); const relatedGrid = document.getElementById("related-grid"); const logo = document.getElementById("logo"); const searchInput = document.getElementById("search-input"); const searchButton = document.getElementById("search-button"); const detailTitle = document.getElementById("detail-title"); const detailAvatar = document.getElementById("detail-avatar"); const detailUploader = document.getElementById("detail-uploader"); const detailUploaded = document.getElementById("detail-uploaded"); const detailViews = document.getElementById("detail-views"); const detailDuration = document.getElementById("detail-duration"); const detailDescription = document.getElementById("detail-description"); const videoCardTemplate = document.getElementById("video-card-template"); let activeWs = null; let hlsInstance = null; let player = null; let prepareInProgress = false; let currentVideo = null; const trendingApiUrl = "/api/trending"; const searchApiUrl = "/api/search"; const wsProtocol = window.location.protocol === "https:" ? "wss://" : "ws://"; const wsBaseUrl = wsProtocol + window.location.host + "/api/v1/prepare"; function formatViews(views) { if (views >= 1000000) { return `${(views / 1000000).toFixed(1)}M`; } else if (views >= 1000) { return `${(views / 1000).toFixed(1)}K`; } else { return views.toString(); } } function formatDuration(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; } else { return `${minutes}:${secs.toString().padStart(2, "0")}`; } } function getVideoIdFromUrl() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('v'); } function getVideoElement() { const existingVideo = playerWrapper.querySelector("video"); if (existingVideo) { existingVideo.remove(); } const videoElement = document.createElement("video"); videoElement.id = "video-player"; videoElement.setAttribute("controls", ""); videoElement.setAttribute("crossorigin", ""); videoElement.setAttribute("playsinline", ""); playerWrapper.appendChild(videoElement); return videoElement; } function cleanupResources() { if (activeWs && activeWs.readyState === WebSocket.OPEN) { activeWs.close(); activeWs = null; } if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } if (player) { player.destroy(); player = null; } prepareInProgress = false; } const observeElements = () => { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add("visible"); observer.unobserve(entry.target); } }); }, { root: null, threshold: 0.1, rootMargin: "0px 0px -50px 0px", } ); document.querySelectorAll(".animated-item:not(.visible)").forEach((item) => { observer.observe(item); }); }; function openVideoPlayer(video) { sessionStorage.setItem('currentVideo', JSON.stringify(video)); const videoId = video.url.replace("/watch?v=", ""); window.location.href = `video.html?v=${videoId}`; } function createVideoCard(video, index) { if (video.type !== "stream" && video.type !== undefined) { return null; } const card = videoCardTemplate.content.cloneNode(true); const thumbnail = card.querySelector(".thumbnail"); const duration = card.querySelector(".duration"); const title = card.querySelector(".video-title"); const uploaderAvatar = card.querySelector(".uploader-avatar"); const uploaderName = card.querySelector(".uploader-name"); const viewCount = card.querySelector(".view-count"); const uploadTime = card.querySelector(".upload-time"); const videoCard = card.querySelector(".video-card"); try { thumbnail.src = video.thumbnail || ""; thumbnail.alt = video.title || "Video thumbnail"; if (typeof video.duration === "number") { duration.textContent = formatDuration(video.duration); } else { duration.textContent = "0:00"; } title.textContent = video.title || "Untitled video"; uploaderAvatar.src = video.uploaderAvatar || ""; uploaderAvatar.alt = `${video.uploaderName || "Uploader"} avatar`; uploaderName.textContent = video.uploaderName || "Unknown uploader"; viewCount.textContent = formatViews(video.views || 0); uploadTime.textContent = video.uploadedDate || "Unknown date"; videoCard.style.animationDelay = `${index * 0.05}s`; videoCard.addEventListener("click", (e) => { e.preventDefault(); openVideoPlayer(video); }); } catch (error) { console.error("Error creating video card:", error, video); } return card; } async function findVideoById(videoId) { const storedVideo = sessionStorage.getItem('currentVideo'); if (storedVideo) { const video = JSON.parse(storedVideo); if (video.url.includes(videoId)) { return video; } } const storedTrending = sessionStorage.getItem('trendingVideos'); if (storedTrending) { const trendingVideos = JSON.parse(storedTrending); const foundInTrending = trendingVideos.find((v) => v.url.includes(videoId)); if (foundInTrending) { return foundInTrending; } } const storedSearch = sessionStorage.getItem('searchResults'); if (storedSearch) { const searchResults = JSON.parse(storedSearch); const foundInSearch = searchResults.find((v) => v.url.includes(videoId)); if (foundInSearch) { return foundInSearch; } } try { console.log('Video not found in cache, fetching from trending API...'); const response = await fetch(trendingApiUrl); if (response.ok) { const videos = await response.json(); const foundVideo = videos.find((v) => v.url.includes(videoId)); if (foundVideo) { return foundVideo; } } const lastSearchQuery = sessionStorage.getItem('lastSearchQuery'); if (lastSearchQuery) { console.log('Trying search API with last query...'); const searchUrl = `${searchApiUrl}?q=${encodeURIComponent(lastSearchQuery)}&filter=all`; const searchResponse = await fetch(searchUrl); if (searchResponse.ok) { const searchData = await searchResponse.json(); const searchResults = searchData.items ? searchData.items.filter((item) => item.type === "stream") : []; const foundInSearch = searchResults.find((v) => v.url.includes(videoId)); if (foundInSearch) { return foundInSearch; } } } } catch (error) { console.error("Error fetching video from API:", error); } return null; } function initializePlayer(streamUrl) { const videoElement = getVideoElement(); if (player) { player.destroy(); } player = new Plyr(videoElement, { controls: [ "play-large", "play", "progress", "current-time", "mute", "volume", "settings", "pip", "airplay", "fullscreen", ], settings: ["captions", "quality", "speed"], tooltips: { controls: true, seek: true }, keyboard: { focused: true, global: true }, }); if (videoElement.canPlayType("application/vnd.apple.mpegurl")) { videoElement.src = streamUrl; } else if (Hls.isSupported()) { if (hlsInstance) { hlsInstance.destroy(); } hlsInstance = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60, }); hlsInstance.loadSource(streamUrl); hlsInstance.attachMedia(videoElement); hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { videoElement.play().catch((e) => { console.warn("Autoplay prevented:", e); }); }); } else { console.error("HLS is not supported in this browser"); preparationStatus.textContent = "Your browser does not support HLS playback"; } } function prepareVideo(videoUrl) { if (prepareInProgress) { return; } prepareInProgress = true; preparationOverlay.style.display = "flex"; preparationStatus.textContent = "Preparing video..."; preparationProgressBar.style.width = "0%"; reloadButton.style.display = "none"; const cleanVideoId = videoUrl.replace("/watch?v=", ""); if (activeWs && activeWs.readyState === WebSocket.OPEN) { activeWs.close(); } activeWs = new WebSocket(wsBaseUrl); activeWs.onopen = () => { activeWs.send(JSON.stringify({ videoId: cleanVideoId })); }; activeWs.onmessage = (event) => { const data = JSON.parse(event.data); if (data.ok === "true") { preparationStatus.textContent = "Connection established..."; } else if (data.preparing !== undefined) { const percentage = parseFloat(data.preparing); preparationProgressBar.style.width = `${percentage}%`; preparationStatus.textContent = `Preparing video... ${percentage.toFixed(0)}%`; } else if (data.done && data.streamUrl) { preparationStatus.textContent = "Video ready! Starting playback..."; preparationProgressBar.style.width = "100%"; setTimeout(() => { preparationOverlay.style.display = "none"; initializePlayer(data.streamUrl); prepareInProgress = false; }, 500); activeWs.close(); activeWs = null; } else { preparationStatus.textContent = "Video playback failed. Try reloading the page."; reloadButton.style.display = "block"; prepareInProgress = false; } }; activeWs.onerror = (error) => { console.error("WebSocket error:", error); preparationStatus.textContent = "Error preparing video. Please try again."; reloadButton.style.display = "block"; prepareInProgress = false; setTimeout(() => { if (preparationOverlay.style.display !== "none") { preparationOverlay.style.display = "none"; } }, 3000); }; activeWs.onclose = () => { console.log("WebSocket connection closed"); if (activeWs === activeWs) { activeWs = null; } }; } function displayVideoDetails(video) { document.title = `${video.title} - Repiped`; detailTitle.textContent = video.title; detailAvatar.src = video.uploaderAvatar; detailUploader.textContent = video.uploaderName; detailUploaded.textContent = video.uploadedDate; detailViews.textContent = formatViews(video.views); detailDuration.textContent = formatDuration(video.duration); detailDescription.textContent = video.shortDescription || "No description available"; const videoDetailUploader = document.querySelector(".video-detail-uploader"); if (videoDetailUploader) { videoDetailUploader.style.cursor = "pointer"; videoDetailUploader.addEventListener("click", () => { if (video.uploaderUrl) { const channelId = video.uploaderUrl.replace("/channel/", ""); window.location.href = `index.html#channelId=${channelId}`; } }); } } async function fetchRelatedVideos(currentVideo) { try { let videos = []; const storedTrending = sessionStorage.getItem('trendingVideos'); if (storedTrending) { videos = JSON.parse(storedTrending); } else { const response = await fetch(trendingApiUrl); if (!response.ok) { throw new Error("Network response was not ok"); } videos = await response.json(); } videos = videos .filter((video) => video.url !== currentVideo.url) .slice(0, 8); relatedGrid.innerHTML = ""; videos.forEach((video, index) => { const card = createVideoCard(video, index); if (card) { relatedGrid.appendChild(card); } }); observeElements(); } catch (error) { console.error("Error fetching related videos:", error); relatedGrid.innerHTML = `
Error loading related videos.