diff --git a/public/index.html b/public/index.html index 5ac94cd..a4c836f 100644 --- a/public/index.html +++ b/public/index.html @@ -1,180 +1,186 @@ + + + + Repiped + + + + + + - - - - Repiped - - - - + +
+
+ - -
- - - - - -
- -
- - -
-
-

Search Results

- 0 results -
-
-
-
-
- -
-
- -
-
-
- Channel banner -
-
-
- Channel avatar -
-
-
-

- -
-
- -
-
-
-
-
-
-
-
-
- - -
-
- -
-
-
-
-
- Preparing video... -
-
-
-
-
-
-

-
-
- Uploader avatar -
- - -
+ + + + +
+
+ +
+ + +
+
+

Search Results

+ 0 results +
+
+
+
+
+ +
+
+ +
+
+
+ Channel banner +
+
+
+ Channel avatar
-
-
+
+
+

+ +
+
+ +
+
+
+
+
+ +
+
+
+
+ + +
+
+
+ + - -
-
- - - - - - - - - - \ No newline at end of file + + + diff --git a/public/script.js b/public/script.js index d4a3039..5f31ff2 100644 --- a/public/script.js +++ b/public/script.js @@ -13,6 +13,7 @@ document.addEventListener("DOMContentLoaded", () => { 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"); @@ -161,28 +162,57 @@ document.addEventListener("DOMContentLoaded", () => { 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) { + if (videoId === activeVideoId && currentVideoData) { 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)); + const video = await findVideoById(videoId); if (video) { + currentVideoData = video; openVideoPlayer(video); } else { - console.error("Video not found in trending"); + console.error("Video not found in any source"); trendingLoader.style.display = "none"; alert("Video not found. Showing trending videos instead."); goToTrendingPage(); @@ -197,6 +227,7 @@ document.addEventListener("DOMContentLoaded", () => { cleanupResources(); activeVideoId = null; + currentVideoData = null; activeChannelId = null; @@ -656,6 +687,8 @@ document.addEventListener("DOMContentLoaded", () => { function openVideoPlayer(video) { cleanupResources(); + currentVideoData = video; + detailTitle.textContent = video.title; detailAvatar.src = video.uploaderAvatar; detailUploader.textContent = video.uploaderName; @@ -887,7 +920,7 @@ document.addEventListener("DOMContentLoaded", () => { loadMoreButton.style.display = "none"; loadMoreSpinner.style.display = "block"; - const url = `https: + const url = `https://pipedapi.wireway.ch/nextpage/channel/${activeChannelId}?nextpage=${encodeURIComponent( channelNextPageData )}`; console.log(`Fetching next page data from: ${url}`); @@ -1012,4 +1045,4 @@ document.addEventListener("DOMContentLoaded", () => { fetchTrendingVideos(); checkInitialHash(); setupInfiniteScroll(); -}); +}); \ No newline at end of file diff --git a/public/style.css b/public/style.css index 840993b..d64b2bc 100644 --- a/public/style.css +++ b/public/style.css @@ -1,252 +1,164 @@ :root { --background: #000000; - --surface: #0f0f0f; - --surface-light: #1a1a1a; + --foreground: #ffffff; + --card: #0a0a0a; + --card-foreground: #ffffff; + --popover: #0a0a0a; + --popover-foreground: #ffffff; --primary: #ffffff; - --secondary: #e0e0e0; - --text-primary: #ffffff; - --text-secondary: #a0a0a0; - --hover-overlay: rgba(255, 255, 255, 0.1); - --shadow: 0 4px 12px rgba(0, 0, 0, 0.5); - --radius: 8px; - --transition: all 0.25s ease; - --transition-fast: all 0.15s ease; - --blur-amount: 10px; + --primary-foreground: #000000; + --secondary: #1a1a1a; + --secondary-foreground: #ffffff; + --muted: #1a1a1a; + --muted-foreground: #a1a1aa; + --accent: #1a1a1a; + --accent-foreground: #ffffff; + --destructive: #dc2626; + --destructive-foreground: #ffffff; + --border: #27272a; + --input: #27272a; + --ring: #ffffff; + --radius: 0.5rem; } * { margin: 0; padding: 0; box-sizing: border-box; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, Cantarell, sans-serif; } body { background-color: var(--background); - color: var(--text-primary); - min-height: 100vh; - overflow-x: hidden; + color: var(--foreground); + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } header { - background-color: var(--surface); - padding: 12px 16px; + border-bottom: 1px solid var(--border); + background-color: var(--background); position: sticky; top: 0; - z-index: 100; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05); + z-index: 50; + backdrop-filter: blur(8px); + background-color: rgba(0, 0, 0, 0.8); +} + +.header-content { display: flex; - justify-content: space-between; align-items: center; - transition: var(--transition); + justify-content: space-between; + padding: 1rem 1.5rem; + max-width: 1400px; + margin: 0 auto; } .logo { display: flex; align-items: center; - gap: 10px; + gap: 0.5rem; cursor: pointer; + transition: opacity 0.2s; +} + +.logo:hover { + opacity: 0.8; } .logo h1 { - font-size: 1.2rem; - color: var(--text-primary); - font-weight: 500; - letter-spacing: 0.5px; - margin-bottom: 1px; + font-size: 1.25rem; + font-weight: 700; + color: var(--foreground); } .logo-icon { - color: var(--text-primary); - font-size: 1rem; + color: var(--foreground); + font-size: 1.125rem; } -.search-bar { +.search-container { display: flex; align-items: center; - gap: 8px; flex: 1; - max-width: 500px; - margin: 0 20px; + max-width: 32rem; + margin: 0 2rem; +} + +.search-form { + display: flex; + width: 100%; + position: relative; } .search-input { flex: 1; - padding: 10px 16px; - border-radius: 30px; - border: none; - background-color: var(--surface-light); - color: var(--text-primary); - font-size: 1rem; - transition: var(--transition); - background-color: var(--surface-light); + height: 2.5rem; + padding: 0 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + border-top-right-radius: 0; + border-bottom-right-radius: 0; + background-color: var(--background); + color: var(--foreground); + font-size: 0.875rem; + transition: border-color 0.2s, box-shadow 0.2s; } .search-input:focus { outline: none; - background-color: var(--hover-overlay); + border-color: var(--ring); + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1); +} + +.search-input::placeholder { + color: var(--muted-foreground); } .search-button { - background-color: var(--surface-light); - border: none; - border-radius: 50%; - height: 40px; - width: 40px; + height: 2.5rem; + padding: 0 1rem; + border: 1px solid var(--border); + border-left: none; + border-radius: var(--radius); + border-top-left-radius: 0; + border-bottom-left-radius: 0; + background-color: var(--secondary); + color: var(--secondary-foreground); + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; display: flex; align-items: center; justify-content: center; - color: var(--text-primary); - cursor: pointer; - transition: var(--transition); } .search-button:hover { - background-color: var(--hover-overlay); + background-color: var(--accent); } .github-button { - display: flex; + display: inline-flex; align-items: center; justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - background-color: var(--surface-light); - color: var(--text-primary); - font-size: 1rem; - font-weight: 700; - transition: var(--transition); - cursor: pointer; + width: 2.5rem; + height: 2.5rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background-color: var(--background); + color: var(--foreground); text-decoration: none; - margin-right: 10px; + transition: background-color 0.2s, border-color 0.2s; } .github-button:hover { - background-color: var(--hover-overlay); + background-color: var(--secondary); } .container { - max-width: 1600px; + max-width: 1400px; margin: 0 auto; - padding: 24px; - perspective: 1000px; -} - -.trending-grid, .search-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 12px; -} - -.search-grid { - margin-top: 32px; -} - -.video-card { - background-color: var(--surface); - border-radius: var(--radius); - overflow: hidden; - cursor: pointer; - box-shadow: none; - transition: var(--transition); - position: relative; - transform: translateY(0); - border: 1px solid rgba(255, 255, 255, 0.05); -} - -.video-card:hover { - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); - border-color: rgba(255, 255, 255, 0.1); -} - -.thumbnail-container { - position: relative; - overflow: hidden; - padding-top: 56.25%; -} - -.thumbnail { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.5s ease; -} - -.video-card:hover .thumbnail { - transform: scale(1.05); -} - -.duration { - position: absolute; - bottom: 8px; - right: 8px; - background-color: rgba(0, 0, 0, 0.8); - color: white; - padding: 2px 6px; - border-radius: 4px; - font-size: 0.8rem; - font-weight: 500; -} - -.video-info { - padding: 16px; -} - -.video-title { - font-size: 0.95rem; - margin-bottom: 8px; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - transition: var(--transition); - letter-spacing: 0.2px; - line-height: 1.4; -} - -.video-card:hover .video-title { - color: var(--secondary); -} - -.video-meta { - display: flex; - flex-direction: column; - gap: 6px; -} - -.uploader { - color: var(--text-secondary); - font-size: 0.9rem; - display: flex; - align-items: center; - gap: 8px; -} - -.uploader-avatar { - width: 24px; - height: 24px; - border-radius: 50%; - object-fit: cover; -} - -.video-stats { - display: flex; - font-size: 0.8rem; - color: var(--text-secondary); - gap: 12px; -} - -.stat { - display: flex; - align-items: center; - gap: 4px; + padding: 2rem 1.5rem; } .page { @@ -256,163 +168,237 @@ header { top: 0; left: 0; width: 100%; - height: auto; - transition: opacity 0.3s ease, transform 0.3s ease, filter 0.3s ease; - transform-origin: center top; - transform: scale(1); - filter: blur(0); - pointer-events: none; + transition: opacity 0.2s ease-in-out; } -.active { +.page.active { opacity: 1; visibility: visible; position: relative; - transform: scale(1); - filter: blur(0); - pointer-events: auto; } -.page.zoom-out { - opacity: 0; - transform: scale(0.95); - filter: blur(var(--blur-amount)); +.trending-grid, +.search-grid, +.channel-videos-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); + gap: 1.5rem; } -.page.zoom-in { - opacity: 0; - transform: scale(1.05); - filter: blur(var(--blur-amount)); +.related-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + gap: 1rem; } -.transition-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100vh; - z-index: 1000; - pointer-events: none; - display: flex; - justify-content: center; - align-items: center; - overflow: hidden; - opacity: 0; - visibility: hidden; - background-color: var(--background); - transform: scale(0); - border-radius: 50%; - transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), - opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), - border-radius 0.4s cubic-bezier(0.16, 1, 0.3, 1); -} - -.transition-overlay.active { - opacity: 1; - visibility: visible; - transform: scale(2); - border-radius: 0; -} - -.skeleton { - background: linear-gradient( - 90deg, - var(--surface), - var(--surface-light), - var(--surface) - ); - background-size: 200% 100%; - animation: shine 1.5s infinite; +.video-card { + background-color: var(--card); + border: 1px solid var(--border); border-radius: var(--radius); + overflow: hidden; + cursor: pointer; + transition: border-color 0.2s, box-shadow 0.2s; } -@keyframes shine { - 0% { - background-position: 200% 0; - } - - 100% { - background-position: -200% 0; - } +.video-card:hover { + border-color: var(--muted-foreground); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } -.skeleton-video { - height: 200px; +.thumbnail-container { + position: relative; + aspect-ratio: 16 / 9; + overflow: hidden; + background-color: var(--muted); } -.skeleton-title { - height: 20px; - margin: 10px 0; - width: 90%; +.thumbnail { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.2s; } -.skeleton-uploader { - height: 16px; - width: 60%; - margin-bottom: 8px; +.video-card:hover .thumbnail { + transform: scale(1.02); } -.skeleton-stats { - height: 14px; - width: 40%; +.duration { + position: absolute; + bottom: 0.5rem; + right: 0.5rem; + background-color: rgba(0, 0, 0, 0.8); + color: white; + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; +} + +.video-info { + padding: 1rem; +} + +.video-title { + font-size: 0.875rem; + font-weight: 600; + margin-bottom: 0.5rem; + line-height: 1.25; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + color: var(--card-foreground); +} + +.video-meta { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.uploader { + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--muted-foreground); + font-size: 0.875rem; +} + +.uploader-avatar { + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + object-fit: cover; +} + +.video-stats { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.75rem; + color: var(--muted-foreground); +} + +.stat { + display: flex; + align-items: center; + gap: 0.25rem; } .video-player-container { display: flex; flex-direction: column; - gap: 24px; + gap: 1.5rem; } .player-wrapper { position: relative; - padding-bottom: 56.25%; - - height: 0; - overflow: hidden; + aspect-ratio: 16 / 9; + background-color: #000; border-radius: var(--radius); - background-color: black; - box-shadow: var(--shadow); + border: 1px solid var(--border); } -.player-wrapper iframe { - position: absolute; - top: 0; - left: 0; +.player-wrapper video { width: 100%; height: 100%; - border: none; + object-fit: contain; + background-color: #000; +} + +.preparation-overlay { + position: absolute; + inset: 0; + background-color: rgba(0, 0, 0, 0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + z-index: 10; +} + +.preparation-status { + font-size: 0.875rem; + font-weight: 500; + color: var(--foreground); + text-align: center; +} + +.preparation-progress-container { + width: 16rem; + height: 0.25rem; + background-color: var(--muted); + border-radius: 0.125rem; + overflow: hidden; +} + +.preparation-progress-bar { + height: 100%; + background-color: var(--primary); + border-radius: 0.125rem; + transition: width 0.3s ease; + width: 0%; +} + +.reload-button { + display: inline-flex; + align-items: center; + justify-content: center; + height: 2.25rem; + padding: 0 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background-color: var(--secondary); + color: var(--secondary-foreground); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.reload-button:hover { + background-color: var(--accent); } .video-detail { - background-color: var(--surface); - padding: 24px; + background-color: var(--card); + border: 1px solid var(--border); border-radius: var(--radius); - box-shadow: var(--shadow); + padding: 1.5rem; } .video-detail-title { font-size: 1.5rem; - margin-bottom: 16px; + font-weight: 700; + margin-bottom: 1rem; + line-height: 1.25; } .video-detail-meta { display: flex; justify-content: space-between; - align-items: center; - margin-bottom: 24px; - flex-wrap: wrap; - gap: 16px; + align-items: start; + margin-bottom: 1.5rem; + gap: 1rem; } .video-detail-uploader { display: flex; align-items: center; - gap: 12px; + gap: 0.75rem; + cursor: pointer; + transition: opacity 0.2s; +} + +.video-detail-uploader:hover { + opacity: 0.8; } .uploader-avatar-large { - width: 48px; - height: 48px; + width: 3rem; + height: 3rem; border-radius: 50%; object-fit: cover; } @@ -427,269 +413,63 @@ header { font-size: 1rem; } +.uploaded-date { + font-size: 0.875rem; + color: var(--muted-foreground); +} + .video-detail-stats { display: flex; - gap: 24px; + gap: 1.5rem; align-items: center; } .detail-stat { display: flex; align-items: center; - gap: 8px; - color: var(--text-secondary); + gap: 0.5rem; + color: var(--muted-foreground); + font-size: 0.875rem; } -.detail-stat i { - font-size: 1.2rem; -} - -.related-videos { - margin-top: 32px; -} - -.section-title { - font-size: 1.1rem; - margin-bottom: 20px; - position: relative; - display: inline-block; - letter-spacing: 0.5px; - font-weight: 400; -} - -.section-title::after { - content: ""; - position: absolute; - left: 0; - bottom: -4px; - width: 40px; - height: 1px; - background-color: var(--secondary); -} - -.related-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 20px; +.video-description { + color: var(--muted-foreground); + line-height: 1.6; + font-size: 0.875rem; } .search-heading { - margin-bottom: 16px; + margin-bottom: 1.5rem; display: flex; align-items: center; - gap: 12px; + gap: 0.75rem; } .search-query { font-size: 1.5rem; - font-weight: 500; - letter-spacing: 0.3px; - color: var(--text-primary); + font-weight: 700; } .search-count { - font-size: 0.9rem; - color: var(--text-secondary); - background-color: var(--surface-light); - padding: 4px 8px; - border-radius: 4px; -} - -.no-results { - text-align: center; - padding: 40px 0; - color: var(--text-secondary); - font-size: 1.1rem; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(20px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -.animated-item { - opacity: 0; - transform: translateY(20px); - transition: opacity 0.5s ease, transform 0.5s ease; -} - -.animated-item.visible { - opacity: 1; - transform: translateY(0); -} - -.preparation-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.9); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - z-index: 10; - border-radius: var(--radius); -} - -.preparation-status { - font-size: 1rem; - margin-bottom: 16px; - color: var(--text-primary); - text-align: center; - letter-spacing: 0.3px; -} - -.preparation-progress-container { - width: 80%; - max-width: 300px; - background-color: var(--surface-light); - height: 4px; - border-radius: 2px; - overflow: hidden; - position: relative; -} - -.preparation-progress-bar { - height: 100%; - width: 0%; - background-color: var(--primary); - border-radius: 2px; - transition: width 0.3s ease; - position: relative; - overflow: hidden; -} - -.preparation-progress-bar::after { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient( - 90deg, - rgba(255, 255, 255, 0.1), - rgba(255, 255, 255, 0.2), - rgba(255, 255, 255, 0.1) - ); - animation: shimmer 1.5s infinite; - transform: translateX(-100%); -} - -.reload-button { - margin-top: 20px; - padding: 10px 20px; - background-color: var(--surface-light); - border: none; - border-radius: var(--radius); - color: var(--text-primary); - font-size: 0.95rem; - cursor: pointer; - transition: var(--transition); - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); -} - -.reload-button:hover { - background-color: var(--hover-overlay); - transform: translateY(-2px); -} - -@keyframes shimmer { - 100% { - transform: translateX(100%); - } -} - -.loading-container { - display: flex; - justify-content: center; - align-items: center; - height: 60vh; -} - -.loader { - width: 48px; - height: 48px; - border: 5px solid var(--surface-light); - border-bottom-color: var(--primary); - border-radius: 50%; - animation: rotation 1s linear infinite; -} - -@keyframes rotation { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -@media (max-width: 768px) { - .trending-grid, - .search-grid { - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - } - - .video-detail-meta { - flex-direction: column; - align-items: flex-start; - } - - .container { - padding: 16px; - } - - .search-bar { - max-width: 100%; - margin: 16px 0 0; - order: 3; - width: 100%; - } - - header { - flex-wrap: wrap; - } -} - -@media (max-width: 480px) { - .trending-grid, - .search-grid { - grid-template-columns: 1fr; - } - - .related-grid { - grid-template-columns: 1fr; - } - - .video-detail-stats { - flex-wrap: wrap; - } + font-size: 0.875rem; + color: var(--muted-foreground); + background-color: var(--muted); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; } .channel-header { + background-color: var(--card); + border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; - margin-bottom: 32px; - background-color: var(--surface); - box-shadow: var(--shadow); + margin-bottom: 2rem; } .channel-banner-container { - position: relative; - height: 150px; + height: 10rem; + background-color: var(--muted); overflow: hidden; - background-color: var(--surface-light); } .channel-banner { @@ -699,105 +479,218 @@ header { } .channel-info { + padding: 1.5rem; display: flex; - padding: 24px; - position: relative; - gap: 20px; + gap: 1rem; } .channel-avatar-container { flex-shrink: 0; + margin-top: -2rem; } .channel-avatar { - width: 80px; - height: 80px; + width: 5rem; + height: 5rem; border-radius: 50%; - border: 3px solid var(--surface); - box-shadow: var(--shadow); - background-color: var(--surface-light); + border: 3px solid var(--background); + background-color: var(--muted); } .channel-details { - flex-grow: 1; + flex: 1; } .channel-title-container { display: flex; align-items: center; - gap: 8px; - margin-bottom: 8px; + gap: 0.5rem; + margin-bottom: 0.5rem; } .channel-title { font-size: 1.5rem; - font-weight: 500; - letter-spacing: 0.3px; + font-weight: 700; } .channel-verified { - color: var(--text-secondary); - font-size: 1.1rem; + color: var(--muted-foreground); } .channel-stats { - color: var(--text-secondary); - font-size: 0.9rem; - margin-bottom: 16px; + color: var(--muted-foreground); + font-size: 0.875rem; + margin-bottom: 1rem; } .channel-description { - color: var(--text-primary); - line-height: 1.5; - font-size: 0.95rem; + color: var(--muted-foreground); + line-height: 1.6; + font-size: 0.875rem; white-space: pre-line; - max-height: 100px; + max-height: 6rem; overflow-y: auto; - margin-top: 12px; -} - -.channel-videos-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 24px; } .load-more-container { display: flex; justify-content: center; - align-items: center; - margin-top: 40px; - flex-direction: column; - min-height: 60px; + margin-top: 2rem; } .load-more-button { - background-color: var(--surface); - color: var(--text-primary); - border: none; - padding: 12px 24px; + display: inline-flex; + align-items: center; + justify-content: center; + height: 2.5rem; + padding: 0 1.5rem; + border: 1px solid var(--border); border-radius: var(--radius); + background-color: var(--secondary); + color: var(--secondary-foreground); + font-size: 0.875rem; + font-weight: 500; cursor: pointer; - font-size: 1rem; - transition: var(--transition); - box-shadow: var(--shadow); + transition: background-color 0.2s; } .load-more-button:hover { - background-color: var(--surface-light); - transform: translateY(-2px); + background-color: var(--accent); } -.load-more-error { - color: #ff5555; - background-color: rgba(255, 85, 85, 0.1); +.section-title { + font-size: 1.25rem; + font-weight: 700; + margin-bottom: 1.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} + +.loading-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 20rem; +} + +.loader { + width: 2rem; + height: 2rem; + border: 2px solid var(--muted); + border-top: 2px solid var(--foreground); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.skeleton { + background: linear-gradient(90deg, + var(--muted) 25%, + var(--secondary) 50%, + var(--muted) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; border-radius: var(--radius); - padding: 12px 24px; +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } +} + +.skeleton-video { + aspect-ratio: 16 / 9; +} + +.skeleton-title { + height: 1rem; + margin: 0.5rem 0; + width: 90%; +} + +.skeleton-uploader { + height: 0.875rem; + width: 60%; + margin-bottom: 0.5rem; +} + +.skeleton-stats { + height: 0.75rem; + width: 40%; +} + +.no-results { text-align: center; - cursor: pointer; + padding: 3rem 1rem; + color: var(--muted-foreground); +} + +.no-results i { + margin-bottom: 1rem; + opacity: 0.5; +} + +.animated-item { + opacity: 0; + transform: translateY(1rem); + transition: opacity 0.4s ease, transform 0.4s ease; +} + +.animated-item.visible { + opacity: 1; + transform: translateY(0); } @media (max-width: 768px) { + .header-content { + flex-direction: column; + gap: 1rem; + } + + .search-container { + order: 3; + margin: 0; + width: 100%; + } + + .container { + padding: 1rem; + } + + .trending-grid, + .search-grid, + .channel-videos-grid { + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + gap: 1rem; + } + + .related-grid { + grid-template-columns: 1fr; + } + + .video-detail-meta { + flex-direction: column; + align-items: flex-start; + } + + .video-detail-stats { + flex-wrap: wrap; + } + .channel-info { flex-direction: column; align-items: center; @@ -805,24 +698,20 @@ header { } .channel-avatar-container { - margin-top: -50px; - } - - .channel-title-container { - justify-content: center; - } - - .channel-description { - text-align: left; - } - - .channel-videos-grid { - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + margin-top: -3rem; } } @media (max-width: 480px) { - .channel-videos-grid { + + .trending-grid, + .search-grid, + .channel-videos-grid, + .related-grid { grid-template-columns: 1fr; } -} + + .video-detail-title { + font-size: 1.25rem; + } +} \ No newline at end of file diff --git a/public/trending.js b/public/trending.js new file mode 100644 index 0000000..e05ce5d --- /dev/null +++ b/public/trending.js @@ -0,0 +1,644 @@ +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", (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 = ` +
+ +

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

No videos found

+

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

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"); + } + + fetchTrendingVideos(); + } + + 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"); + } + + 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

+

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

Failed to load channel

+

Error loading channel. Please try again later.

+
+ `; + } + } + + 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. Click to try again.

+
+ `; + + 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; + } + } + + function openChannelPage(channelId) { + fetchChannelData(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; + } + + 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(); + } + }); + + fetchTrendingVideos(); + checkInitialHash(); + setupInfiniteScroll(); +}); \ No newline at end of file diff --git a/public/video.html b/public/video.html new file mode 100644 index 0000000..ed4ed87 --- /dev/null +++ b/public/video.html @@ -0,0 +1,152 @@ + + + + + + Repiped - Video Player + + + + + + + + + +
+
+ + +
+
+ + +
+
+ + + + +
+
+ +
+
+
+
+
+ Preparing video... +
+
+
+
+ +
+
+ +
+

Loading...

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

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; + } + + 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", () => { + 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(); +}); \ No newline at end of file