forked from obvtiger/repiped
1015 lines
31 KiB
JavaScript
1015 lines
31 KiB
JavaScript
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;
|
|
|
|
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 fetchVideoDetails(videoId) {
|
|
try {
|
|
if (videoId === activeVideoId) {
|
|
return;
|
|
}
|
|
|
|
activeVideoId = videoId;
|
|
|
|
trendingLoader.style.display = "flex";
|
|
|
|
const response = await fetch(trendingApiUrl);
|
|
if (!response.ok) {
|
|
throw new Error("Network response was not ok");
|
|
}
|
|
|
|
const videos = await response.json();
|
|
const video = videos.find((v) => v.url.includes(videoId));
|
|
|
|
if (video) {
|
|
openVideoPlayer(video);
|
|
} else {
|
|
console.error("Video not found in trending");
|
|
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;
|
|
|
|
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 = `
|
|
<div class="no-results">
|
|
<i class="fas fa-exclamation-circle fa-3x" style="margin-bottom: 16px; color: #ff5555;"></i>
|
|
<p>Error loading videos. Please try again later.</p>
|
|
<p style="font-size: 0.9rem; margin-top: 8px; color: var(--text-secondary);">${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<div class="no-results">
|
|
<i class="fas fa-search fa-3x" style="margin-bottom: 16px; color: var(--text-secondary);"></i>
|
|
<p>No video results found for "${lastSearchQuery}"</p>
|
|
</div>
|
|
`;
|
|
} 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 = `
|
|
<div class="no-results">
|
|
<i class="fas fa-exclamation-circle fa-3x" style="margin-bottom: 16px; color: #ff5555;"></i>
|
|
<p>Error searching videos. Please try again later.</p>
|
|
<p style="font-size: 0.9rem; margin-top: 8px; color: var(--text-secondary);">${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
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 = `<p>Error loading related videos.</p>`;
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<div class="no-results">
|
|
<i class="fas fa-video fa-3x" style="margin-bottom: 16px; color: var(--text-secondary);"></i>
|
|
<p>No videos available</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 = `
|
|
<div class="no-results">
|
|
<i class="fas fa-exclamation-circle fa-3x" style="margin-bottom: 16px; color: #ff5555;"></i>
|
|
<p>Error loading channel. Please try again later.</p>
|
|
<p style="font-size: 0.9rem; margin-top: 8px; color: var(--text-secondary);">${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
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:
|
|
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 = `
|
|
<div class="load-more-error">Error loading more videos. Tap to try again.</div>
|
|
`;
|
|
|
|
setTimeout(() => {
|
|
loadMoreContainer.innerHTML = `
|
|
<div class="loader" id="channel-load-more-spinner" style="display: none;"></div>
|
|
<button class="load-more-button" id="channel-load-more-button">Load More</button>
|
|
`;
|
|
|
|
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();
|
|
});
|