import { useState, useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from "@tanstack/react-query"; import { getChannel, getChannelVideos, searchChannelYoutube, followChannel, unfollowChannel, indexChannel, indexChannelFull, exploreChannelOlder, fetchPopularVideos, downloadChannel, getChannelPlaylists, fetchChannelPlaylists, getPlaylistVideos, indexPlaylist, } from "../api"; import VideoCard from "../components/VideoCard"; const LIMIT = 60; const TABS = [ { value: "videos", label: "Videos" }, { value: "popular", label: "Popular" }, { value: "playlists", label: "Playlists" }, ]; const SORTS = [ { value: "newest", label: "Newest" }, { value: "oldest", label: "Oldest" }, { value: "title", label: "A–Z" }, { value: "unwatched", label: "Unwatched" }, ]; function formatSubs(n) { if (!n) return null; if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1)}M`; if (n >= 1_000) return `${Math.round(n / 1_000)}K`; return String(n); } export default function ChannelPage() { const { id } = useParams(); const qc = useQueryClient(); const [tab, setTab] = useState("videos"); const [sort, setSort] = useState("newest"); const [search, setSearch] = useState(""); const [activeQ, setActiveQ] = useState(""); const [indexing, setIndexing] = useState(false); const [explorePage, setExplorePage] = useState(2); const searchInputRef = useRef(null); const [openPlaylistId, setOpenPlaylistId] = useState(null); const [playlistOffset, setPlaylistOffset] = useState(0); const { data: channel, isLoading: loadingChannel } = useQuery({ queryKey: ["channel", id], queryFn: () => getChannel(id).then((r) => r.data), }); const effectiveSort = tab === "popular" ? "popular" : sort; const { data: videosData, isLoading: loadingVideos, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ["channel-videos", id, effectiveSort, activeQ], queryFn: ({ pageParam = 0 }) => getChannelVideos(id, effectiveSort, pageParam, LIMIT, activeQ).then((r) => r.data), getNextPageParam: (lastPage, pages) => lastPage.length === LIMIT ? pages.length * LIMIT : undefined, enabled: !!id, }); const videos = videosData?.pages.flat() ?? []; // Refetch after background re-index const refetchedRef = useRef(false); useEffect(() => { if (!id || refetchedRef.current) return; refetchedRef.current = true; const t = setTimeout(() => { qc.invalidateQueries({ queryKey: ["channel", id] }); qc.invalidateQueries({ queryKey: ["channel-videos", id] }); }, 8000); return () => clearTimeout(t); }, [id, qc]); const followMut = useMutation({ mutationFn: () => channel?.status === "followed" ? unfollowChannel(id) : followChannel(id), onSuccess: () => { qc.invalidateQueries({ queryKey: ["channel", id] }); qc.invalidateQueries({ queryKey: ["channels"] }); }, }); const scheduleRefetch = (delayMs) => { setIndexing(true); setTimeout(() => { qc.invalidateQueries({ queryKey: ["channel-videos", id] }); qc.invalidateQueries({ queryKey: ["channel", id] }); setIndexing(false); }, delayMs); }; const indexMut = useMutation({ mutationFn: () => indexChannel(id), onSuccess: () => scheduleRefetch(6000), }); const fullIndexMut = useMutation({ mutationFn: () => indexChannelFull(id), onSuccess: () => scheduleRefetch(45000), }); const exploreMut = useMutation({ mutationFn: () => exploreChannelOlder(id, explorePage), onSuccess: () => { setExplorePage(p => p + 1); scheduleRefetch(20000); }, }); const popularMut = useMutation({ mutationFn: () => fetchPopularVideos(id), onSuccess: () => scheduleRefetch(20000), }); const deepSearchMut = useMutation({ mutationFn: () => searchChannelYoutube(id, activeQ || search), onSuccess: () => scheduleRefetch(20000), }); const { data: playlists = [], refetch: refetchPlaylists } = useQuery({ queryKey: ["channel-playlists", id], queryFn: () => getChannelPlaylists(id).then((r) => r.data), enabled: !!id && tab === "playlists", }); const fetchPlaylistsMut = useMutation({ mutationFn: () => fetchChannelPlaylists(id), onSuccess: () => setTimeout(() => refetchPlaylists(), 8000), }); const { data: playlistVideos, isLoading: loadingPlaylistVideos, refetch: refetchPlaylistVideos } = useQuery({ queryKey: ["playlist-videos", openPlaylistId, playlistOffset], queryFn: () => getPlaylistVideos(openPlaylistId, playlistOffset, 60).then((r) => r.data), enabled: !!openPlaylistId, }); const indexPlaylistMut = useMutation({ mutationFn: (plId) => indexPlaylist(plId), onSuccess: () => setTimeout(() => refetchPlaylistVideos(), 15000), }); const [dlResult, setDlResult] = useState(null); const dlMut = useMutation({ mutationFn: () => downloadChannel(id), onSuccess: (res) => { setDlResult(res.data.queued); qc.invalidateQueries({ queryKey: ["downloads"] }); }, }); const handleSearch = (e) => { e.preventDefault(); setActiveQ(search.trim()); }; const clearSearch = () => { setSearch(""); setActiveQ(""); searchInputRef.current?.focus(); }; if (loadingChannel) { return (
); } if (!channel) return

Channel not found.

; const isFollowed = channel.status === "followed"; const isPending = indexMut.isPending || fullIndexMut.isPending || deepSearchMut.isPending || exploreMut.isPending || popularMut.isPending || indexing; return (
{/* Banner */}
{channel.banner_url && ( )}
{channel.thumbnail_url ? ( {channel.name} ) : (
{channel.name?.[0]?.toUpperCase()}
)}

{channel.name}

{[ formatSubs(channel.subscriber_count) && `${formatSubs(channel.subscriber_count)} subs`, channel.video_count && `${channel.video_count} indexed`, ].filter(Boolean).join(" · ")}

{/* Mobile actions */}
{dlResult != null && (

{dlResult === 0 ? "Already up to date" : `${dlResult} downloads queued`}

)} {channel.description && (

{channel.description}

)} {/* Search bar */}
setSearch(e.target.value)} placeholder="Search videos…" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:outline-none focus:border-zinc-600 transition-colors" /> {search && ( )}
{activeQ && ( )}
{/* Tabs + controls */}
{TABS.map(t => ( ))}
{isPending && ( Fetching… )} {tab === "popular" ? ( ) : ( <> )}
{/* Sort bar — videos tab only */} {tab === "videos" && (
{SORTS.map(s => ( ))}
)} {/* Playlist browser */} {tab === "playlists" && ( openPlaylistId ? (
{(() => { const pl = playlists.find(p => p.id === openPlaylistId); return pl ? {pl.title} : null; })()}
{loadingPlaylistVideos ? (
) : playlistVideos?.length ? ( <>
{playlistVideos.map((v) => ( ))}
{playlistOffset > 0 && ( )} {playlistVideos.length === 60 && ( )}
) : (

No videos indexed for this playlist yet.

)}
) : (
{playlists.length} playlists
{playlists.length === 0 ? (

No playlists fetched yet.

) : (
{playlists.map(pl => ( ))}
)}
) )} {/* Video list */} {tab !== "playlists" && (loadingVideos ? (
) : videos.length ? (
{videos.map((v) => ( ))} {hasNextPage ? ( ) : !activeQ && tab === "videos" && (

{videos.length} videos indexed

·
)}
) : (

{activeQ ? `No indexed videos match "${activeQ}"` : tab === "popular" ? "No popular videos fetched yet." : "No videos indexed yet."}

{activeQ ? ( ) : tab === "popular" ? ( ) : ( )}
))}
); }