diff --git a/backend/app/routes/channels.py b/backend/app/routes/channels.py
index dd04589..3325d50 100644
--- a/backend/app/routes/channels.py
+++ b/backend/app/routes/channels.py
@@ -223,7 +223,13 @@ def _channel_summary(ch: Channel) -> dict:
def _channel_detail_dict(
- channel: Channel, *, subscribed: bool, explored: bool, blocked: bool, stored: int
+ channel: Channel,
+ *,
+ subscribed: bool,
+ explored: bool,
+ blocked: bool,
+ stored: int,
+ deep_requested: bool,
) -> dict:
return {
"id": channel.id,
@@ -245,6 +251,10 @@ def _channel_detail_dict(
"explored": explored,
"blocked": blocked,
"stored_videos": stored,
+ # Deep-backfill state, so the channel page can offer "get full history" when the catalog
+ # holds only the recent-backfill/RSS subset (video_count is YouTube's raw total).
+ "deep_requested": deep_requested,
+ "backfill_done": channel.backfill_done,
"from_explore": channel.from_explore,
"details_synced": channel.details_synced_at is not None,
}
@@ -274,11 +284,12 @@ def channel_detail(
except YouTubeError as exc:
log.warning("channel detail enrich failed (%s): %s", channel_id, exc)
db.rollback()
- subscribed = db.execute(
- select(Subscription.id).where(
+ sub = db.execute(
+ select(Subscription).where(
Subscription.user_id == user.id, Subscription.channel_id == channel_id
)
- ).first() is not None
+ ).scalar_one_or_none()
+ subscribed = sub is not None
explored = db.execute(
select(ExploredChannel.id).where(
ExploredChannel.user_id == user.id, ExploredChannel.channel_id == channel_id
@@ -287,7 +298,12 @@ def channel_detail(
blocked = _is_blocked(db, user, channel_id)
stored = db.scalar(select(func.count(Video.id)).where(Video.channel_id == channel_id)) or 0
return _channel_detail_dict(
- channel, subscribed=subscribed, explored=explored, blocked=blocked, stored=int(stored)
+ channel,
+ subscribed=subscribed,
+ explored=explored,
+ blocked=blocked,
+ stored=int(stored),
+ deep_requested=bool(sub.deep_requested) if sub else False,
)
diff --git a/frontend/src/components/ChannelPage.tsx b/frontend/src/components/ChannelPage.tsx
index 320e800..a8bef92 100644
--- a/frontend/src/components/ChannelPage.tsx
+++ b/frontend/src/components/ChannelPage.tsx
@@ -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({
)}
+ {/* 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 && (
+
+ )}
{!me.is_demo && (