feat(channels): surface full-history pull + honest stored/total count on the channel page (S3)

The catalog only stores the recent-backfill/RSS subset of a channel's uploads (video_count is
YouTube's raw total), so a subscribed channel looked like it was "missing" videos. Now the channel
page header shows "{stored} / {total} videos" when there's a gap, and a "Full history" action queues
the deep backfill (the same deep_requested trigger the channel manager uses) — showing a loading
state until done. Backend: channel_detail now returns deep_requested/backfill_done.
This commit is contained in:
2026-07-28 02:22:36 +02:00
parent ae14aa1c6a
commit eb922cb589
5 changed files with 80 additions and 8 deletions
+45 -1
View File
@@ -8,6 +8,7 @@ import {
Ban,
Check,
ExternalLink,
History,
Loader2,
Plus,
RefreshCw,
@@ -150,6 +151,21 @@ export default function ChannelPage({
},
onError: (err) => notifyYouTubeActionError(err, t("channels.notify.unsubscribeFailed")),
});
// "Get full history": the catalog only holds the recent-backfill/RSS subset of a channel's
// uploads (video_count is YouTube's raw total), so flipping deep_requested queues the full
// back-catalog pager. Same trigger the channel manager uses.
const deepBackfill = useMutation({
mutationFn: () => api.updateChannel(channelId, { deep_requested: true }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: qk.channel(channelId) });
qc.invalidateQueries({ queryKey: qk.feed() });
qc.invalidateQueries({ queryKey: qk.channels() });
qc.invalidateQueries({ queryKey: qk.myStatus() });
notify({ level: "info", message: t("channel.fullHistoryQueued") });
},
onError: (err) => notifyYouTubeActionError(err, t("channel.fullHistoryFailed")),
});
const onUnsubscribe = async () => {
const ok = await confirm({
title: t("channel.unsubTitle"),
@@ -191,7 +207,14 @@ export default function ChannelPage({
ch?.subscriber_count != null
? t("channel.subscribers", { formatted: formatViews(ch.subscriber_count) })
: null,
ch?.video_count != null ? t("channel.videoCount", { count: ch.video_count }) : null,
// Show "stored / total" when we only hold a subset of a subscribed channel's uploads, so the
// gap between the browsable videos and YouTube's raw count is honest rather than looking like
// missing data (the "get full history" button pulls the rest).
ch?.video_count != null
? ch.subscribed && !ch.backfill_done && ch.stored_videos < ch.video_count
? t("channel.videoCountStored", { stored: ch.stored_videos, total: ch.video_count })
: t("channel.videoCount", { count: ch.video_count })
: null,
ch?.total_view_count != null
? t("channel.totalViews", { formatted: formatViews(ch.total_view_count) })
: null,
@@ -330,6 +353,27 @@ export default function ChannelPage({
<SlidersHorizontal className="w-4 h-4" />
</button>
)}
{/* Only the recent subset is stored → offer to pull the channel's full back-catalogue.
Once queued (deep_requested) it shows as loading; hidden once the backfill is done. */}
{ch?.subscribed && !ch.backfill_done && !me.is_demo && (
<button
onClick={() => !ch.deep_requested && deepBackfill.mutate()}
disabled={ch.deep_requested || deepBackfill.isPending}
title={
ch.deep_requested ? t("channel.fullHistoryLoading") : t("channel.fullHistory")
}
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm border border-border text-muted hover:text-accent hover:border-accent disabled:opacity-60 disabled:hover:text-muted disabled:hover:border-border transition"
>
{ch.deep_requested ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<History className="w-4 h-4" />
)}
<span className="hidden sm:inline">
{ch.deep_requested ? t("channel.fullHistoryLoading") : t("channel.fullHistory")}
</span>
</button>
)}
{!me.is_demo && (
<button
onClick={() => block.mutate()}
+6 -1
View File
@@ -10,6 +10,7 @@
"subscribers": "{{formatted}} subscribers",
"videoCount_one": "{{count}} video",
"videoCount_other": "{{count}} videos",
"videoCountStored": "{{stored}} / {{total}} videos",
"totalViews": "{{formatted}} views",
"joined": "Joined {{date}}",
"loadingVideos": "Loading this channel's videos…",
@@ -24,5 +25,9 @@
"language": "Language",
"topics": "Topics",
"keywords": "Keywords",
"showInFeed": "Show in my feed"
"showInFeed": "Show in my feed",
"fullHistory": "Full history",
"fullHistoryLoading": "Fetching history…",
"fullHistoryQueued": "Fetching this channel's full history — new videos will appear as they're found.",
"fullHistoryFailed": "Couldn't start the full-history fetch."
}
+6 -1
View File
@@ -10,6 +10,7 @@
"subscribers": "{{formatted}} feliratkozó",
"videoCount_one": "{{count}} videó",
"videoCount_other": "{{count}} videó",
"videoCountStored": "{{stored}} / {{total}} videó",
"totalViews": "{{formatted}} megtekintés",
"joined": "Csatlakozott: {{date}}",
"loadingVideos": "A csatorna videóinak betöltése…",
@@ -24,5 +25,9 @@
"language": "Nyelv",
"topics": "Témák",
"keywords": "Kulcsszavak",
"showInFeed": "Mutasd a hírfolyamomban"
"showInFeed": "Mutasd a hírfolyamomban",
"fullHistory": "Teljes előzmény",
"fullHistoryLoading": "Előzmény letöltése…",
"fullHistoryQueued": "A csatorna teljes előzményének letöltése elindult — az új videók feltűnnek, ahogy előkerülnek.",
"fullHistoryFailed": "Nem sikerült elindítani a teljes előzmény letöltését."
}
+2
View File
@@ -136,6 +136,8 @@ export interface ChannelDetail {
explored: boolean;
blocked: boolean;
stored_videos: number;
deep_requested: boolean;
backfill_done: boolean;
from_explore: boolean;
details_synced: boolean;
}