document.addEventListener("DOMContentLoaded", () => { const overlay = document.createElement("div"); overlay.id = "transition-overlay"; overlay.className = "transition-overlay"; document.body.appendChild(overlay); let activeVideoId = null; let prepareInProgress = false; let activeWs = null; let hlsInstance = null; let previousPage = "trending-page"; let lastSearchQuery = ""; let activeChannelId = null; let channelNextPageData = null; let isLoadingMoreVideos = false; let currentVideoData = null; const trendingPage = document.getElementById("trending-page"); const searchResultsPage = document.getElementById("search-results-page"); const channelPage = document.getElementById("channel-page"); const videoPlayerPage = document.getElementById("video-player-page"); const trendingGrid = document.getElementById("trending-grid"); const trendingLoader = document.getElementById("trending-loader"); const searchGrid = document.getElementById("search-grid"); const searchLoader = document.getElementById("search-loader"); const searchQueryText = document.getElementById("search-query"); const searchCountText = document.getElementById("search-count"); const searchInput = document.getElementById("search-input"); const searchButton = document.getElementById("search-button"); const relatedGrid = document.getElementById("related-grid"); const playerWrapper = document.getElementById("player-wrapper"); const logo = document.getElementById("logo"); const channelVideosGrid = document.getElementById("channel-videos-grid"); const channelLoader = document.getElementById("channel-loader"); const channelBanner = document.getElementById("channel-banner"); const channelAvatar = document.getElementById("channel-avatar"); const channelTitle = document.getElementById("channel-title"); const channelVerified = document.getElementById("channel-verified"); const channelSubs = document.getElementById("channel-subs"); const channelDescription = document.getElementById("channel-description"); const preparationOverlay = document.getElementById("preparation-overlay"); const preparationStatus = document.getElementById("preparation-status"); const preparationProgressBar = document.getElementById( "preparation-progress-bar" ); 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"); let player = null; const videoCardTemplate = document.getElementById("video-card-template"); const skeletonTemplate = document.getElementById("skeleton-template"); const trendingApiUrl = "/api/trending"; const searchApiUrl = "/api/search"; const wsProcotol = window.location.protocol === "https:" ? "wss://" : "ws://"; const wsBaseUrl = wsProcotol + window.location.host + "/api/v1/prepare"; const reloadButton = document.getElementById("reload-button"); reloadButton.addEventListener("click", () => { window.location.reload(); }); function switchPage(fromPageId, toPageId) { const fromPage = document.getElementById(fromPageId); const toPage = document.getElementById(toPageId); if (!fromPage || !toPage) return; if (toPageId !== "video-player-page") { previousPage = fromPageId; } fromPage.classList.add("zoom-out"); setTimeout(() => { fromPage.classList.remove("active"); fromPage.classList.remove("zoom-out"); toPage.classList.add("zoom-in"); toPage.classList.add("active"); requestAnimationFrame(() => { requestAnimationFrame(() => { toPage.classList.remove("zoom-in"); }); }); }, 300); } function getHashParams() { const hash = window.location.hash.substring(1); const params = {}; hash.split("&").forEach((param) => { if (param) { const [key, value] = param.split("="); params[key] = decodeURIComponent(value); } }); return params; } function updateHash(params = {}) { const hashParts = []; Object.entries(params).forEach(([key, value]) => { if (value) { hashParts.push(`${key}=${encodeURIComponent(value)}`); } }); if (hashParts.length > 0) { history.pushState("", document.title, `#${hashParts.join("&")}`); } else { history.pushState("", document.title, window.location.pathname); } } 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; } 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; } async function findVideoById(videoId) { if (currentVideoData && currentVideoData.url.includes(videoId)) { return currentVideoData; } try { const trendingResponse = await fetch(trendingApiUrl); if (trendingResponse.ok) { const trendingVideos = await trendingResponse.json(); const foundInTrending = trendingVideos.find((v) => v.url.includes(videoId)); if (foundInTrending) { return foundInTrending; } } if (lastSearchQuery) { 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; } } } return null; } catch (error) { console.error("Error searching for video:", error); return null; } } async function fetchVideoDetails(videoId) { try { if (videoId === activeVideoId && currentVideoData) { return; } activeVideoId = videoId; trendingLoader.style.display = "flex"; const video = await findVideoById(videoId); if (video) { currentVideoData = video; openVideoPlayer(video); } else { console.error("Video not found in any source"); trendingLoader.style.display = "none"; alert("Video not found. Showing trending videos instead."); goToTrendingPage(); } } catch (error) { console.error("Error fetching video details:", error); trendingLoader.style.display = "none"; } } function goToTrendingPage() { cleanupResources(); activeVideoId = null; currentVideoData = null; activeChannelId = null; updateHash(); if (videoPlayerPage.classList.contains("active")) { switchPage("video-player-page", "trending-page"); } else if (searchResultsPage.classList.contains("active")) { switchPage("search-results-page", "trending-page"); } else if (channelPage.classList.contains("active")) { switchPage("channel-page", "trending-page"); } fetchTrendingVideos(); } function checkInitialHash() { const params = getHashParams(); if (params.videoId) { fetchVideoDetails(params.videoId); } else if (params.search) { performSearch(params.search); } else if (params.channelId) { fetchChannelData(params.channelId); } } window.addEventListener("hashchange", () => { const params = getHashParams(); if (params.videoId) { fetchVideoDetails(params.videoId); } else if (params.search) { performSearch(params.search); } else if (params.channelId) { fetchChannelData(params.channelId); } else { goToTrendingPage(); } }); 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 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 showSkeletons(container, count = 12) { container.innerHTML = ""; for (let i = 0; i < count; i++) { const skeleton = skeletonTemplate.content.cloneNode(true); skeleton.firstElementChild.style.animationDelay = `${i * 0.05}s`; container.appendChild(skeleton); } container.style.display = "grid"; if (container === trendingGrid) { trendingLoader.style.display = "none"; } else if (container === searchGrid) { searchLoader.style.display = "none"; } } 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 formatUploadTime(timestamp) { const now = new Date(); const uploadDate = new Date(timestamp); const diffMs = now - uploadDate; const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays < 1) { const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); if (diffHours < 1) { const diffMinutes = Math.floor(diffMs / (1000 * 60)); return `${diffMinutes} minutes ago`; } return `${diffHours} hours ago`; } else if (diffDays < 7) { return `${diffDays} days ago`; } else if (diffDays < 30) { const diffWeeks = Math.floor(diffDays / 7); return `${diffWeeks} weeks ago`; } else if (diffDays < 365) { const diffMonths = Math.floor(diffDays / 30); return `${diffMonths} months ago`; } else { const diffYears = Math.floor(diffDays / 365); return `${diffYears} years ago`; } } function formatSubscribers(count) { if (count >= 1000000) { return `${(count / 1000000).toFixed(1)}M subscribers`; } else if (count >= 1000) { return `${(count / 1000).toFixed(1)}K subscribers`; } else { return `${count} subscribers`; } } function createVideoCard(video, index) { if (video.type !== "stream" && video.type !== undefined) { console.log( `Skipping non-video item: ${video.type} - ${video.title || video.name}` ); 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"); const uploader = card.querySelector(".uploader"); 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) => { if ( e.target === uploaderAvatar || e.target === uploaderName || (e.target.parentElement && e.target.parentElement === uploader) ) { e.stopPropagation(); if (video.uploaderUrl) { openChannelPage(video.uploaderUrl.replace("/channel/", "")); } } else { openVideoPlayer(video); } }); uploader.addEventListener("click", (e) => { e.stopPropagation(); if (video.uploaderUrl) { openChannelPage(video.uploaderUrl.replace("/channel/", "")); } }); } catch (error) { console.error("Error creating video card:", error, video); } return card; } async function fetchTrendingVideos() { try { trendingLoader.style.display = "flex"; trendingGrid.style.display = "none"; const response = await fetch(trendingApiUrl); if (!response.ok) { throw new Error("Network response was not ok"); } const videos = await response.json(); showSkeletons(trendingGrid); setTimeout(() => { trendingGrid.innerHTML = ""; videos.forEach((video, index) => { const card = createVideoCard(video, index); if (card) { trendingGrid.appendChild(card); } }); observeElements(); }, 600); } catch (error) { console.error("Error fetching trending videos:", error); trendingLoader.style.display = "none"; trendingGrid.style.display = "block"; trendingGrid.innerHTML = `

Error loading videos. Please try again later.

${error.message}

`; } } async function performSearch(query) { if (!query || query.trim() === "") return; lastSearchQuery = query.trim(); updateHash({ search: lastSearchQuery }); if (trendingPage.classList.contains("active")) { switchPage("trending-page", "search-results-page"); } else if (videoPlayerPage.classList.contains("active")) { switchPage("video-player-page", "search-results-page"); } else if (channelPage.classList.contains("active")) { switchPage("channel-page", "search-results-page"); } searchQueryText.textContent = `"${lastSearchQuery}"`; searchCountText.textContent = "Searching..."; searchGrid.style.display = "none"; searchLoader.style.display = "flex"; try { const url = `${searchApiUrl}?q=${encodeURIComponent( lastSearchQuery )}&filter=all`; console.log(`Fetching search results from: ${url}`); const response = await fetch(url); if (!response.ok) { throw new Error(`Network response was not ok: ${response.status}`); } const searchData = await response.json(); const results = searchData.items ? searchData.items.filter((item) => item.type === "stream") : []; console.log( `Found ${results.length} video results out of ${searchData.items ? searchData.items.length : 0 } total items` ); searchCountText.textContent = `${results.length} videos`; if (results.length === 0) { searchLoader.style.display = "none"; searchGrid.style.display = "block"; searchGrid.innerHTML = `

No video results found for "${lastSearchQuery}"

`; } else { showSkeletons(searchGrid, Math.min(12, results.length)); setTimeout(() => { searchGrid.innerHTML = ""; results.forEach((video, index) => { const card = createVideoCard(video, index); searchGrid.appendChild(card); }); observeElements(); }, 300); } } catch (error) { console.error("Error fetching search results:", error); searchLoader.style.display = "none"; searchGrid.style.display = "block"; searchGrid.innerHTML = `

Error searching videos. Please try again later.

${error.message}

`; } } 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(videoId) { if (prepareInProgress) { return; } prepareInProgress = true; preparationOverlay.style.display = "flex"; preparationStatus.textContent = "Preparing video..."; preparationProgressBar.style.width = "0%"; const cleanVideoId = videoId.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. Reloading the page fixes this."; preparationProgressBar.style.width = "100%"; } }; activeWs.onerror = (error) => { console.error("WebSocket error:", error); preparationStatus.textContent = "Error preparing video. Please try again."; prepareInProgress = false; setTimeout(() => { preparationOverlay.style.display = "none"; }, 3000); }; activeWs.onclose = () => { console.log("WebSocket connection closed"); if (activeWs === activeWs) { activeWs = null; } }; } function openVideoPlayer(video) { cleanupResources(); currentVideoData = video; 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) { openChannelPage(video.uploaderUrl.replace("/channel/", "")); } }); } if (trendingPage.classList.contains("active")) { switchPage("trending-page", "video-player-page"); } else if (searchResultsPage.classList.contains("active")) { switchPage("search-results-page", "video-player-page"); } else if (channelPage.classList.contains("active")) { switchPage("channel-page", "video-player-page"); } window.scrollTo(0, 0); const videoId = video.url.replace("/watch?v=", ""); updateHash({ videoId }); activeVideoId = videoId; prepareVideo(video.url); fetchRelatedVideos(video); } async function fetchRelatedVideos(currentVideo) { try { const response = await fetch(trendingApiUrl); if (!response.ok) { throw new Error("Network response was not ok"); } let 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.

`; } } async function fetchChannelData(channelId) { try { if (channelId === activeChannelId) { return; } activeChannelId = channelId; channelNextPageData = null; updateHash({ channelId }); if (trendingPage.classList.contains("active")) { switchPage("trending-page", "channel-page"); } else if (searchResultsPage.classList.contains("active")) { switchPage("search-results-page", "channel-page"); } else if (videoPlayerPage.classList.contains("active")) { switchPage("video-player-page", "channel-page"); } channelLoader.style.display = "flex"; channelVideosGrid.style.display = "none"; document.getElementById("channel-load-more").style.display = "none"; channelTitle.textContent = ""; channelSubs.textContent = ""; channelDescription.textContent = ""; channelAvatar.src = ""; channelBanner.src = ""; channelVerified.style.display = "none"; const url = `https://pipedapi.wireway.ch/channel/${channelId}`; console.log(`Fetching channel data from: ${url}`); const response = await fetch(url); if (!response.ok) { throw new Error(`Network response was not ok: ${response.status}`); } const channelData = await response.json(); console.log("Channel data:", channelData); if (!channelData || typeof channelData !== "object") { throw new Error("Invalid channel data received"); } channelNextPageData = channelData.nextpage || null; channelTitle.textContent = channelData.name || "Unknown Channel"; channelSubs.textContent = formatSubscribers( channelData.subscriberCount || 0 ); if (channelData.description) { channelDescription.textContent = channelData.description.replace( /\\n/g, "\n" ); } else { channelDescription.textContent = "No description available"; } if (channelData.avatarUrl) { channelAvatar.src = channelData.avatarUrl; channelAvatar.style.display = "block"; } else { channelAvatar.style.display = "none"; } if (channelData.bannerUrl) { channelBanner.src = channelData.bannerUrl; channelBanner.style.display = "block"; } else { channelBanner.style.display = "none"; } channelVerified.style.display = channelData.verified ? "inline-block" : "none"; const relatedStreams = Array.isArray(channelData.relatedStreams) ? channelData.relatedStreams : []; const videoCount = relatedStreams.length; showSkeletons(channelVideosGrid, Math.min(12, videoCount || 6)); setTimeout(() => { channelVideosGrid.innerHTML = ""; if (relatedStreams.length > 0) { relatedStreams.forEach((video, index) => { if (!video.uploaderName) { video.uploaderName = channelData.name || "Unknown Channel"; } if (!video.uploaderUrl) { video.uploaderUrl = `/channel/${channelData.id}`; } if (!video.uploaderAvatar) { video.uploaderAvatar = channelData.avatarUrl || ""; } const card = createVideoCard(video, index); if (card) { channelVideosGrid.appendChild(card); } }); observeElements(); const loadMoreContainer = document.getElementById("channel-load-more"); if (channelNextPageData) { loadMoreContainer.style.display = "flex"; } else { loadMoreContainer.style.display = "none"; } } else { channelVideosGrid.innerHTML = `

No videos available

`; } channelLoader.style.display = "none"; channelVideosGrid.style.display = "grid"; }, 300); } catch (error) { console.error("Error fetching channel data:", error); channelLoader.style.display = "none"; channelVideosGrid.style.display = "block"; channelVideosGrid.innerHTML = `

Error loading channel. Please try again later.

${error.message}

`; } } async function loadMoreChannelVideos() { if (!channelNextPageData || isLoadingMoreVideos) { return; } try { isLoadingMoreVideos = true; const loadMoreButton = document.getElementById( "channel-load-more-button" ); const loadMoreSpinner = document.getElementById( "channel-load-more-spinner" ); loadMoreButton.style.display = "none"; loadMoreSpinner.style.display = "block"; const url = `https://pipedapi.wireway.ch/nextpage/channel/${activeChannelId}?nextpage=${encodeURIComponent( channelNextPageData )}`; console.log(`Fetching next page data from: ${url}`); const response = await fetch(url); if (!response.ok) { throw new Error(`Network response was not ok: ${response.status}`); } const nextPageData = await response.json(); console.log("Next page data:", nextPageData); if (!nextPageData || !Array.isArray(nextPageData.relatedStreams)) { throw new Error("Invalid next page data received"); } channelNextPageData = nextPageData.nextpage || null; const relatedStreams = nextPageData.relatedStreams; const startIndex = document.querySelectorAll( ".channel-videos-grid .video-card" ).length; if (relatedStreams.length > 0) { relatedStreams.forEach((video, index) => { if (!video.uploaderName) { video.uploaderName = channelTitle.textContent || "Unknown Channel"; } if (!video.uploaderUrl) { video.uploaderUrl = `/channel/${activeChannelId}`; } if (!video.uploaderAvatar) { video.uploaderAvatar = channelAvatar.src || ""; } const card = createVideoCard(video, startIndex + index); if (card) { channelVideosGrid.appendChild(card); } }); observeElements(); } if (channelNextPageData) { loadMoreButton.style.display = "block"; } else { document.getElementById("channel-load-more").style.display = "none"; } } catch (error) { console.error("Error loading more videos:", error); const loadMoreContainer = document.getElementById("channel-load-more"); loadMoreContainer.innerHTML = `
Error loading more videos. Tap to try again.
`; setTimeout(() => { loadMoreContainer.innerHTML = ` `; document .getElementById("channel-load-more-button") .addEventListener("click", loadMoreChannelVideos); }, 3000); } finally { document.getElementById("channel-load-more-spinner").style.display = "none"; isLoadingMoreVideos = false; } } function setupInfiniteScroll() { window.addEventListener("scroll", function () { if ( !channelPage.classList.contains("active") || !channelNextPageData || isLoadingMoreVideos ) { return; } const scrollPos = window.scrollY + window.innerHeight; const loadMoreContainer = document.getElementById("channel-load-more"); const loadMorePosition = loadMoreContainer.offsetTop; if (scrollPos > loadMorePosition - 300) { loadMoreChannelVideos(); } }); document .getElementById("channel-load-more-button") .addEventListener("click", loadMoreChannelVideos); } function openChannelPage(channelId) { if (videoPlayerPage.classList.contains("active")) { cleanupResources(); } fetchChannelData(channelId); } logo.addEventListener("click", () => { goToTrendingPage(); }); searchButton.addEventListener("click", () => { performSearch(searchInput.value); }); searchInput.addEventListener("keypress", (e) => { if (e.key === "Enter") { performSearch(searchInput.value); } }); fetchTrendingVideos(); checkInitialHash(); setupInfiniteScroll(); });