From c1a4025cbda17b9d6ff7a84231c8b6c810f03899 Mon Sep 17 00:00:00 2001 From: obvTiger Date: Fri, 13 Jun 2025 23:20:44 +0200 Subject: [PATCH] fix: channel id irregular --- public/script.js | 1048 ---------------------------------------- public/trending.js | 1140 +++++++++++++++++++++++--------------------- public/video.js | 925 ++++++++++++++++++----------------- routes/channel.js | 41 ++ routes/search.js | 24 +- 5 files changed, 1152 insertions(+), 2026 deletions(-) delete mode 100644 public/script.js create mode 100644 routes/channel.js diff --git a/public/script.js b/public/script.js deleted file mode 100644 index 5f31ff2..0000000 --- a/public/script.js +++ /dev/null @@ -1,1048 +0,0 @@ -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(); -}); \ No newline at end of file diff --git a/public/trending.js b/public/trending.js index e05ce5d..93af7a7 100644 --- a/public/trending.js +++ b/public/trending.js @@ -1,644 +1,718 @@ document.addEventListener("DOMContentLoaded", () => { - const trendingPage = document.getElementById("trending-page"); - const searchResultsPage = document.getElementById("search-results-page"); - const channelPage = document.getElementById("channel-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 logo = document.getElementById("logo"); - const channelVideosGrid = document.getElementById("channel-videos-grid"); - const channelLoader = document.getElementById("channel-loader"); + const trendingPage = document.getElementById("trending-page"); + const searchResultsPage = document.getElementById("search-results-page"); + const channelPage = document.getElementById("channel-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 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 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 videoCardTemplate = document.getElementById("video-card-template"); - const skeletonTemplate = document.getElementById("skeleton-template"); + const videoCardTemplate = document.getElementById("video-card-template"); + const skeletonTemplate = document.getElementById("skeleton-template"); - let lastSearchQuery = ""; - let activeChannelId = null; - let channelNextPageData = null; - let isLoadingMoreVideos = false; + let lastSearchQuery = ""; + let activeChannelId = null; + let channelNextPageData = null; + let isLoadingMoreVideos = false; - const trendingApiUrl = "/api/trending"; - const searchApiUrl = "/api/search"; + const trendingApiUrl = "/api/trending"; + const searchApiUrl = "/api/search"; - 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 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 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 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 switchPage(fromPageId, toPageId) { + const fromPage = document.getElementById(fromPageId); + const toPage = document.getElementById(toPageId); + + if (!fromPage || !toPage) return; + + 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 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"; + } + } + + 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)); + sessionStorage.setItem("lastSearchQuery", lastSearchQuery); + + 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) { + console.log( + `Skipping non-video item: ${video.type} - ${video.title || video.name}` + ); + return null; } - function formatDuration(seconds) { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; + 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"); - if (hours > 0) { - return `${hours}:${minutes.toString().padStart(2, "0")}:${secs - .toString() - .padStart(2, "0")}`; + 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", async (e) => { + if ( + e.target === uploaderAvatar || + e.target === uploaderName || + (e.target.parentElement && e.target.parentElement === uploader) + ) { + e.stopPropagation(); + if (video.uploaderUrl) { + const username = video.uploaderUrl; + openChannelPage(username); + } } else { - return `${minutes}:${secs.toString().padStart(2, "0")}`; + openVideoPlayer(video); } + }); + + uploader.addEventListener("click", async (e) => { + e.stopPropagation(); + if (video.uploaderUrl) { + const username = video.uploaderUrl; + openChannelPage(username); + } + }); + } catch (error) { + console.error("Error creating video card:", error, video); } - 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`; - } - } + return card; + } - function getHashParams() { - const hash = window.location.hash.substring(1); - const params = {}; + async function fetchTrendingVideos() { + try { + trendingLoader.style.display = "flex"; + trendingGrid.style.display = "none"; - hash.split("&").forEach((param) => { - if (param) { - const [key, value] = param.split("="); - params[key] = decodeURIComponent(value); - } + const response = await fetch(trendingApiUrl); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const videos = await response.json(); + + sessionStorage.setItem("trendingVideos", JSON.stringify(videos)); + + showSkeletons(trendingGrid); + + setTimeout(() => { + trendingGrid.innerHTML = ""; + + videos.forEach((video, index) => { + const card = createVideoCard(video, index); + if (card) { + trendingGrid.appendChild(card); + } }); - return params; - } + observeElements(); + }, 600); + } catch (error) { + console.error("Error fetching trending videos:", error); - 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 switchPage(fromPageId, toPageId) { - const fromPage = document.getElementById(fromPageId); - const toPage = document.getElementById(toPageId); - - if (!fromPage || !toPage) return; - - 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 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"; - } - } - - 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)); - sessionStorage.setItem('lastSearchQuery', lastSearchQuery); - - 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) { - 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(); - - sessionStorage.setItem('trendingVideos', JSON.stringify(videos)); - - 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 = ` + trendingLoader.style.display = "none"; + trendingGrid.style.display = "block"; + trendingGrid.innerHTML = `

Failed to load videos

Please try again later.

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

No videos found

No video results found for "${lastSearchQuery}"

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

Search failed

Error searching videos. Please try again later.

`; - } + } + } + + function goToTrendingPage() { + updateHash(); + + if (searchResultsPage.classList.contains("active")) { + switchPage("search-results-page", "trending-page"); + } else if (channelPage.classList.contains("active")) { + switchPage("channel-page", "trending-page"); } - function goToTrendingPage() { - updateHash(); + fetchTrendingVideos(); + } + async function fetchChannelData(channelId) { + try { + const resolvedChannelId = await resolveChannelId(channelId); - if (searchResultsPage.classList.contains("active")) { - switchPage("search-results-page", "trending-page"); - } else if (channelPage.classList.contains("active")) { - switchPage("channel-page", "trending-page"); + if (resolvedChannelId === activeChannelId) { + return; + } + + activeChannelId = resolvedChannelId; + 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"); + } + + 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"; + let finalChannelId = channelId; + + if (!channelId.startsWith("UC")) { + const url = `/api/channel/${channelId}`; + console.log(`Resolving channel ID from: ${url}`); + + const idResponse = await fetch(url); + if (!idResponse.ok) { + throw new Error(`Network response was not ok: ${idResponse.status}`); } + const idData = await idResponse.json(); + finalChannelId = idData.channelId; + } - fetchTrendingVideos(); - } + const pipedUrl = `https://pipedapi.wireway.ch/channel/${finalChannelId}`; + console.log(`Fetching channel data from: ${pipedUrl}`); + const response = await fetch(pipedUrl); - async function fetchChannelData(channelId) { - try { - if (channelId === activeChannelId) { - return; + 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 || ""; } - 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"); + const card = createVideoCard(video, index); + if (card) { + channelVideosGrid.appendChild(card); } + }); - channelLoader.style.display = "flex"; - channelVideosGrid.style.display = "none"; - document.getElementById("channel-load-more").style.display = "none"; + observeElements(); - 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 = ` + const loadMoreContainer = + document.getElementById("channel-load-more"); + if (channelNextPageData) { + loadMoreContainer.style.display = "flex"; + } else { + loadMoreContainer.style.display = "none"; + } + } else { + channelVideosGrid.innerHTML = `

No videos available

This channel hasn't uploaded any videos yet.

`; - } + } - 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 = ` + 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 = `

Failed to load channel

Error loading channel. Please try again later.

`; - } + } + } + + async function loadMoreChannelVideos() { + if (!channelNextPageData || isLoadingMoreVideos) { + return; } - async function loadMoreChannelVideos() { - if (!channelNextPageData || isLoadingMoreVideos) { - return; - } + try { + isLoadingMoreVideos = true; - 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 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 url = `https://pipedapi.wireway.ch/nextpage/channel/${activeChannelId}?nextpage=${encodeURIComponent(channelNextPageData)}`; - console.log(`Fetching next page data from: ${url}`); + const response = await fetch(url); - const response = await fetch(url); + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.status}`); + } - if (!response.ok) { - throw new Error(`Network response was not ok: ${response.status}`); - } + const nextPageData = await response.json(); + console.log("Next page data:", nextPageData); - 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"); + } - if (!nextPageData || !Array.isArray(nextPageData.relatedStreams)) { - throw new Error("Invalid next page data received"); - } + channelNextPageData = nextPageData.nextpage || null; - channelNextPageData = nextPageData.nextpage || null; + const relatedStreams = nextPageData.relatedStreams; + const startIndex = document.querySelectorAll( + ".channel-videos-grid .video-card" + ).length; - 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 || ""; + } - 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); + } + }); - const card = createVideoCard(video, startIndex + index); - if (card) { - channelVideosGrid.appendChild(card); - } - }); + observeElements(); + } - 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); - 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 = ` + const loadMoreContainer = document.getElementById("channel-load-more"); + loadMoreContainer.innerHTML = `

Error loading more videos. Click to try again.

`; - loadMoreContainer.addEventListener('click', () => { - loadMoreContainer.innerHTML = ` + loadMoreContainer.addEventListener("click", () => { + loadMoreContainer.innerHTML = ` `; - document.getElementById("channel-load-more-button").addEventListener("click", loadMoreChannelVideos); - }); - } finally { - document.getElementById("channel-load-more-spinner").style.display = "none"; - isLoadingMoreVideos = false; - } + document + .getElementById("channel-load-more-button") + .addEventListener("click", loadMoreChannelVideos); + }); + } finally { + document.getElementById("channel-load-more-spinner").style.display = + "none"; + isLoadingMoreVideos = false; } + } - function openChannelPage(channelId) { - fetchChannelData(channelId); + async function openChannelPage(channelIdOrUsername) { + try { + // Remove any existing hash params + const channelId = await resolveChannelId(channelIdOrUsername); + updateHash({ channelId }); + fetchChannelData(channelId); + } catch (error) { + console.error("Error opening channel page:", error); } + } - function checkInitialHash() { - const params = getHashParams(); - if (params.search) { - searchInput.value = params.search; - performSearch(params.search); - } else if (params.channelId) { - fetchChannelData(params.channelId); - } + function checkInitialHash() { + const params = getHashParams(); + if (params.search) { + searchInput.value = params.search; + performSearch(params.search); + } else if (params.channelId) { + fetchChannelData(params.channelId); } + } - function setupInfiniteScroll() { - window.addEventListener("scroll", function () { - if (!channelPage.classList.contains("active") || !channelNextPageData || isLoadingMoreVideos) { - return; - } + 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; + 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); - } - - logo.addEventListener("click", () => { - goToTrendingPage(); + if (scrollPos > loadMorePosition - 300) { + loadMoreChannelVideos(); + } }); - searchButton.addEventListener("click", () => { - performSearch(searchInput.value); - }); + document + .getElementById("channel-load-more-button") + .addEventListener("click", loadMoreChannelVideos); + } - searchInput.addEventListener("keypress", (e) => { - if (e.key === "Enter") { - performSearch(searchInput.value); - } - }); + logo.addEventListener("click", () => { + goToTrendingPage(); + }); - window.addEventListener("hashchange", () => { - const params = getHashParams(); - if (params.search) { - performSearch(params.search); - } else if (params.channelId) { - fetchChannelData(params.channelId); - } else { - goToTrendingPage(); - } - }); + searchButton.addEventListener("click", () => { + performSearch(searchInput.value); + }); - fetchTrendingVideos(); - checkInitialHash(); - setupInfiniteScroll(); -}); \ No newline at end of file + searchInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + performSearch(searchInput.value); + } + }); + + window.addEventListener("hashchange", () => { + const params = getHashParams(); + if (params.search) { + performSearch(params.search); + } else if (params.channelId) { + fetchChannelData(params.channelId); + } else { + goToTrendingPage(); + } + }); + + async function resolveChannelId(username) { + if (!username) return null; + if (!username.startsWith("@")) return username; + + try { + const response = await fetch( + `/api/channel/${encodeURIComponent(username)}` + ); + if (!response.ok) { + console.error("Failed to resolve channel ID for:", username); + return username; + } + const data = await response.json(); + return data.channelId; + } catch (error) { + console.error("Error resolving channel ID:", error); + return username; + } + } + + fetchTrendingVideos(); + checkInitialHash(); + setupInfiniteScroll(); +}); diff --git a/public/video.js b/public/video.js index d01a552..58c3fc4 100644 --- a/public/video.js +++ b/public/video.js @@ -1,479 +1,534 @@ 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 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 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"); + const videoCardTemplate = document.getElementById("video-card-template"); - let activeWs = null; - let hlsInstance = null; - let player = null; - let prepareInProgress = false; - let currentVideo = null; + 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"; + 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 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(); } - function formatDuration(seconds) { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; + const videoElement = document.createElement("video"); + videoElement.id = "video-player"; + videoElement.setAttribute("controls", ""); + videoElement.setAttribute("crossorigin", ""); + videoElement.setAttribute("playsinline", ""); - if (hours > 0) { - return `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; - } else { - return `${minutes}:${secs.toString().padStart(2, "0")}`; - } + playerWrapper.appendChild(videoElement); + return videoElement; + } + + function cleanupResources() { + if (activeWs && activeWs.readyState === WebSocket.OPEN) { + activeWs.close(); + activeWs = null; } - function getVideoIdFromUrl() { - const urlParams = new URLSearchParams(window.location.search); - return urlParams.get('v'); + if (hlsInstance) { + hlsInstance.destroy(); + hlsInstance = null; } - 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; + if (player) { + player.destroy(); + player = null; } - function cleanupResources() { - if (activeWs && activeWs.readyState === WebSocket.OPEN) { - activeWs.close(); - activeWs = null; - } + prepareInProgress = false; + } - 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); + 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", async (e) => { + const uploader = card.querySelector(".uploader"); + if ( + e.target === uploaderAvatar || + e.target === uploaderName || + (e.target.parentElement && e.target.parentElement === uploader) + ) { + e.stopPropagation(); + if (video.uploaderUrl) { + const username = video.uploaderUrl; + const channelId = await getChannelId(username); + window.location.href = `index.html#channelId=${channelId}`; + } + } else { + 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 })); }; - function openVideoPlayer(video) { - sessionStorage.setItem('currentVideo', JSON.stringify(video)); - const videoId = video.url.replace("/watch?v=", ""); - window.location.href = `video.html?v=${videoId}`; + 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; + } + }; + } + async function getChannelId(username) { + try { + if (username.startsWith("UC")) { + return username; + } + + const response = await fetch( + `/api/channel/${encodeURIComponent(username)}` + ); + if (!response.ok) { + throw new Error(`Failed to fetch channel ID: ${response.status}`); + } + const data = await response.json(); + return data.channelId; + } catch (error) { + console.error("Error fetching channel ID:", error); + return username; } + } - function createVideoCard(video, index) { - if (video.type !== "stream" && video.type !== undefined) { - return 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", async () => { + if (video.uploaderUrl) { + const username = video.uploaderUrl; + const channelId = await getChannelId(username); + window.location.href = `index.html#channelId=${channelId}`; } - - 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; - } + 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(); + } - 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; - } + 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); } + }); - 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 = ` + observeElements(); + } catch (error) { + console.error("Error fetching related videos:", error); + relatedGrid.innerHTML = `

Failed to load related videos

Error loading related videos.

`; - } + } + } + + async function performSearch(query) { + if (!query || query.trim() === "") return; + + window.location.href = `index.html#search=${encodeURIComponent( + query.trim() + )}`; + } + + async function loadVideo() { + const videoId = getVideoIdFromUrl(); + + if (!videoId) { + alert("No video ID provided"); + window.location.href = "index.html"; + return; } - async function performSearch(query) { - if (!query || query.trim() === "") return; + console.log(`Loading video: ${videoId}`); - window.location.href = `index.html#search=${encodeURIComponent(query.trim())}`; - } + try { + const video = await findVideoById(videoId); - async function loadVideo() { - const videoId = getVideoIdFromUrl(); - - if (!videoId) { - alert("No video ID provided"); - window.location.href = "index.html"; - return; - } - - console.log(`Loading video: ${videoId}`); - - try { - const video = await findVideoById(videoId); - - if (!video) { - alert("Video not found. Redirecting to home page."); - window.location.href = "index.html"; - return; - } - - currentVideo = video; - - sessionStorage.setItem('currentVideo', JSON.stringify(video)); - - displayVideoDetails(video); - - prepareVideo(video.url); - - fetchRelatedVideos(video); - - } catch (error) { - console.error("Error loading video:", error); - alert("Error loading video. Redirecting to home page."); - window.location.href = "index.html"; - } - } - - logo.addEventListener("click", () => { + if (!video) { + alert("Video not found. Redirecting to home page."); window.location.href = "index.html"; - }); + return; + } - searchButton.addEventListener("click", () => { - performSearch(searchInput.value); - }); + currentVideo = video; - searchInput.addEventListener("keypress", (e) => { - if (e.key === "Enter") { - performSearch(searchInput.value); - } - }); + sessionStorage.setItem("currentVideo", JSON.stringify(video)); - reloadButton.addEventListener("click", () => { - window.location.reload(); - }); + displayVideoDetails(video); - window.addEventListener("beforeunload", () => { - cleanupResources(); - }); + prepareVideo(video.url); - loadVideo(); -}); \ No newline at end of file + fetchRelatedVideos(video); + } catch (error) { + console.error("Error loading video:", error); + alert("Error loading video. Redirecting to home page."); + window.location.href = "index.html"; + } + } + + logo.addEventListener("click", () => { + window.location.href = "index.html"; + }); + + searchButton.addEventListener("click", () => { + performSearch(searchInput.value); + }); + + searchInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + performSearch(searchInput.value); + } + }); + + reloadButton.addEventListener("click", () => { + window.location.reload(); + }); + + window.addEventListener("beforeunload", () => { + cleanupResources(); + }); + + loadVideo(); +}); diff --git a/routes/channel.js b/routes/channel.js new file mode 100644 index 0000000..c78f100 --- /dev/null +++ b/routes/channel.js @@ -0,0 +1,41 @@ +const axios = require("axios"); +const cheerio = require("cheerio"); + +function channelRouteHandler(app) { + app.get("/api/channel/:username", async (req, res) => { + try { + const username = req.params.username; + if (!username) { + return res.status(400).json({ error: "Username is required" }); + } + + const response = await axios.get(`https://www.youtube.com/${username}`); + const $ = cheerio.load(response.data); + const canonicalLink = $('link[rel="canonical"]').attr("href"); + + if (!canonicalLink) { + return res.status(404).json({ error: "Channel not found" }); + } + + const channelIdMatch = canonicalLink.match(/\/channel\/([\w-]+)/); + const channelId = channelIdMatch ? channelIdMatch[1] : null; + + if (!channelId) { + return res.status(404).json({ error: "Could not extract channel ID" }); + } + + return res.json({ + channelId: channelId, + username: username, + }); + } catch (error) { + console.error("Error fetching channel ID:", error.message); + return res.status(500).json({ + error: "Failed to fetch channel ID", + message: error.message, + }); + } + }); +} + +module.exports = channelRouteHandler; diff --git a/routes/search.js b/routes/search.js index beccacb..c2c269a 100644 --- a/routes/search.js +++ b/routes/search.js @@ -1,5 +1,5 @@ const https = require("https"); -const yts = require('yt-search'); +const yts = require("yt-search"); function searchRouteHandler(app) { app.get("/api/search", async (req, res) => { @@ -10,32 +10,36 @@ function searchRouteHandler(app) { } const results = await yts(query); - console.log(results.videos[0]) - + console.log(results.videos[0]); + const formattedResults = { - items: results.videos.map(video => ({ + items: results.videos.map((video) => ({ url: `/watch?v=${video.videoId}`, type: "stream", title: video.title, thumbnail: `/api/thumbnail/${video.videoId}`, uploaderName: video.author.name, - uploaderUrl: `/channel/${video.author.url}`, - uploaderAvatar: `/api/avatar/${video.author.url.replace("https://youtube.com/", "")}.png`, + uploaderUrl: `${video.author.url.replace( + "https://youtube.com/", + "" + )}`, + uploaderAvatar: `/api/avatar/${video.author.url.replace( + "https://youtube.com/", + "" + )}.png`, uploadedDate: video.ago, shortDescription: video.description, duration: video.seconds, views: video.views, uploaded: new Date(video.timestamp * 1000).getTime(), - uploaderVerified: false, - isShort: video.duration.seconds < 60 + isShort: video.duration.seconds < 60, })), nextpage: "", suggestion: "", - corrected: false + corrected: false, }; res.json(formattedResults); - } catch (err) { console.error(`Error searching videos: ${err.message}`); res.status(500).send("Error searching videos");