Add older content exploration: channel page + home feed Rediscover mode

Channel page:
- "Explore older videos" button fetches 100 videos at a time further back
  in the channel history using yt-dlp --playlist-start/--playlist-end
- "Fetch entire history" still available for full crawl
- Backend: /channels/{id}/explore?page=N endpoint + playlist offset support
  in fetch_channel_metadata(start_video=N)

Home feed:
- New "Rediscover" mode: older unwatched videos (90+ days old) from
  followed channels, randomly sampled then re-ranked by tag affinity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 22:17:20 +02:00
parent 0b482b5d49
commit aa91156bbc
6 changed files with 131 additions and 10 deletions

View File

@@ -48,6 +48,7 @@ export const followChannel = (id) => api.post(`/channels/${id}/follow`);
export const unfollowChannel = (id) => api.delete(`/channels/${id}/follow`);
export const indexChannel = (id) => api.post(`/channels/${id}/index`);
export const indexChannelFull = (id) => api.post(`/channels/${id}/index-full`);
export const exploreChannelOlder = (id, page) => api.post(`/channels/${id}/explore`, null, { params: { page } });
export const followChannelByUrl = (data) => api.post("/channels/follow-by-url", data);
export const setChannelAutoDownload = (id, value) => api.patch(`/channels/${id}/auto-download`, { auto_download: value });
export const markChannelsSeen = () => api.post("/channels/mark-seen");

View File

@@ -3,7 +3,7 @@ import { useParams } from "react-router-dom";
import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from "@tanstack/react-query";
import {
getChannel, getChannelVideos, searchChannelYoutube,
followChannel, unfollowChannel, indexChannel, indexChannelFull, downloadChannel,
followChannel, unfollowChannel, indexChannel, indexChannelFull, exploreChannelOlder, downloadChannel,
} from "../api";
import VideoCard from "../components/VideoCard";
@@ -30,6 +30,7 @@ export default function ChannelPage() {
const [search, setSearch] = useState("");
const [activeQ, setActiveQ] = useState("");
const [indexing, setIndexing] = useState(false);
const [explorePage, setExplorePage] = useState(2);
const searchInputRef = useRef(null);
const { data: channel, isLoading: loadingChannel } = useQuery({
@@ -94,6 +95,14 @@ export default function ChannelPage() {
onSuccess: () => scheduleRefetch(45000),
});
const exploreMut = useMutation({
mutationFn: () => exploreChannelOlder(id, explorePage),
onSuccess: () => {
setExplorePage(p => p + 1);
scheduleRefetch(20000);
},
});
const deepSearchMut = useMutation({
mutationFn: () => searchChannelYoutube(id, activeQ || search),
onSuccess: () => scheduleRefetch(20000),
@@ -130,7 +139,7 @@ export default function ChannelPage() {
if (!channel) return <p className="text-zinc-500">Channel not found.</p>;
const isFollowed = channel.status === "followed";
const isPending = indexMut.isPending || fullIndexMut.isPending || deepSearchMut.isPending || indexing;
const isPending = indexMut.isPending || fullIndexMut.isPending || deepSearchMut.isPending || exploreMut.isPending || indexing;
return (
<div className="flex flex-col gap-5">
@@ -282,12 +291,19 @@ export default function ChannelPage() {
{isFetchingNextPage ? "Loading…" : "Load more"}
</button>
) : !activeQ && (
<div className="mt-4 flex flex-col items-center gap-2 py-4 border-t border-zinc-800/50">
<p className="text-xs text-zinc-600">All {videos.length} indexed videos shown</p>
<button onClick={() => fullIndexMut.mutate()} disabled={isPending}
className="text-xs text-zinc-500 hover:text-zinc-300 transition-colors disabled:opacity-40">
Fetch full history from YouTube
</button>
<div className="mt-4 flex flex-col items-center gap-3 py-4 border-t border-zinc-800/50">
<p className="text-xs text-zinc-600">{videos.length} videos indexed</p>
<div className="flex items-center gap-4">
<button onClick={() => exploreMut.mutate()} disabled={isPending}
className="text-xs text-zinc-400 hover:text-zinc-100 font-medium transition-colors disabled:opacity-40">
Explore older videos
</button>
<span className="text-zinc-800 text-xs">·</span>
<button onClick={() => fullIndexMut.mutate()} disabled={isPending}
className="text-xs text-zinc-500 hover:text-zinc-300 transition-colors disabled:opacity-40">
Fetch entire history
</button>
</div>
</div>
)}
</div>

View File

@@ -9,6 +9,7 @@ const PAGE_SIZE = 25;
const FEED_MODES = [
{ value: "ranked", label: "For you", hint: "Ranked by your taste" },
{ value: "chronological", label: "New", hint: "Everything in date order" },
{ value: "rediscover", label: "Rediscover", hint: "Older unwatched videos ranked by your taste" },
{ value: "random", label: "Explore", hint: "Random from discovery pool" },
{ value: "inbox", label: "Inbox", hint: "New from followed channels since last visit" },
];