repiped/public/trending.js

718 lines
22 KiB
JavaScript

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 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");
let lastSearchQuery = "";
let activeChannelId = null;
let channelNextPageData = null;
let isLoadingMoreVideos = false;
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 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;
}
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", 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 {
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);
}
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 = `
<div class="no-results">
<i class="fas fa-exclamation-circle fa-3x"></i>
<h3>Failed to load videos</h3>
<p>Please try again later.</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 (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")
: [];
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 = `
<div class="no-results">
<i class="fas fa-search fa-3x"></i>
<h3>No videos found</h3>
<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"></i>
<h3>Search failed</h3>
<p>Error searching videos. Please try again later.</p>
</div>
`;
}
}
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");
}
fetchTrendingVideos();
}
async function fetchChannelData(channelId) {
try {
const resolvedChannelId = await resolveChannelId(channelId);
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;
}
const pipedUrl = `https://pipedapi.wireway.ch/channel/${finalChannelId}`;
console.log(`Fetching channel data from: ${pipedUrl}`);
const response = await fetch(pipedUrl);
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"></i>
<h3>No videos available</h3>
<p>This channel hasn't uploaded any videos yet.</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"></i>
<h3>Failed to load channel</h3>
<p>Error loading channel. Please try again later.</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://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 = `
<div class="no-results">
<p>Error loading more videos. Click to try again.</p>
</div>
`;
loadMoreContainer.addEventListener("click", () => {
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 Videos</button>
`;
document
.getElementById("channel-load-more-button")
.addEventListener("click", loadMoreChannelVideos);
});
} finally {
document.getElementById("channel-load-more-spinner").style.display =
"none";
isLoadingMoreVideos = false;
}
}
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 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);
}
logo.addEventListener("click", () => {
goToTrendingPage();
});
searchButton.addEventListener("click", () => {
performSearch(searchInput.value);
});
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();
});