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 (
Channel not found.
; const isFollowed = channel.status === "followed"; const isPending = indexMut.isPending || fullIndexMut.isPending || deepSearchMut.isPending || exploreMut.isPending || popularMut.isPending || indexing; return ({[ formatSubs(channel.subscriber_count) && `${formatSubs(channel.subscriber_count)} subs`, channel.video_count && `${channel.video_count} indexed`, ].filter(Boolean).join(" · ")}
{dlResult === 0 ? "Already up to date" : `${dlResult} downloads queued`}
)} {channel.description && ({channel.description}
)} {/* Search bar */} {/* Tabs + controls */}No videos indexed for this playlist yet.
No playlists fetched yet.
{videos.length} videos indexed
{activeQ ? `No indexed videos match "${activeQ}"` : tab === "popular" ? "No popular videos fetched yet." : "No videos indexed yet."}
{activeQ ? ( ) : tab === "popular" ? ( ) : ( )}