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:
@@ -223,7 +223,13 @@ def _channel_summary(ch: Channel) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _channel_detail_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:
|
) -> dict:
|
||||||
return {
|
return {
|
||||||
"id": channel.id,
|
"id": channel.id,
|
||||||
@@ -245,6 +251,10 @@ def _channel_detail_dict(
|
|||||||
"explored": explored,
|
"explored": explored,
|
||||||
"blocked": blocked,
|
"blocked": blocked,
|
||||||
"stored_videos": stored,
|
"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,
|
"from_explore": channel.from_explore,
|
||||||
"details_synced": channel.details_synced_at is not None,
|
"details_synced": channel.details_synced_at is not None,
|
||||||
}
|
}
|
||||||
@@ -274,11 +284,12 @@ def channel_detail(
|
|||||||
except YouTubeError as exc:
|
except YouTubeError as exc:
|
||||||
log.warning("channel detail enrich failed (%s): %s", channel_id, exc)
|
log.warning("channel detail enrich failed (%s): %s", channel_id, exc)
|
||||||
db.rollback()
|
db.rollback()
|
||||||
subscribed = db.execute(
|
sub = db.execute(
|
||||||
select(Subscription.id).where(
|
select(Subscription).where(
|
||||||
Subscription.user_id == user.id, Subscription.channel_id == channel_id
|
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(
|
explored = db.execute(
|
||||||
select(ExploredChannel.id).where(
|
select(ExploredChannel.id).where(
|
||||||
ExploredChannel.user_id == user.id, ExploredChannel.channel_id == channel_id
|
ExploredChannel.user_id == user.id, ExploredChannel.channel_id == channel_id
|
||||||
@@ -287,7 +298,12 @@ def channel_detail(
|
|||||||
blocked = _is_blocked(db, user, channel_id)
|
blocked = _is_blocked(db, user, channel_id)
|
||||||
stored = db.scalar(select(func.count(Video.id)).where(Video.channel_id == channel_id)) or 0
|
stored = db.scalar(select(func.count(Video.id)).where(Video.channel_id == channel_id)) or 0
|
||||||
return _channel_detail_dict(
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Ban,
|
Ban,
|
||||||
Check,
|
Check,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
History,
|
||||||
Loader2,
|
Loader2,
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -150,6 +151,21 @@ export default function ChannelPage({
|
|||||||
},
|
},
|
||||||
onError: (err) => notifyYouTubeActionError(err, t("channels.notify.unsubscribeFailed")),
|
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 onUnsubscribe = async () => {
|
||||||
const ok = await confirm({
|
const ok = await confirm({
|
||||||
title: t("channel.unsubTitle"),
|
title: t("channel.unsubTitle"),
|
||||||
@@ -191,7 +207,14 @@ export default function ChannelPage({
|
|||||||
ch?.subscriber_count != null
|
ch?.subscriber_count != null
|
||||||
? t("channel.subscribers", { formatted: formatViews(ch.subscriber_count) })
|
? t("channel.subscribers", { formatted: formatViews(ch.subscriber_count) })
|
||||||
: null,
|
: 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
|
ch?.total_view_count != null
|
||||||
? t("channel.totalViews", { formatted: formatViews(ch.total_view_count) })
|
? t("channel.totalViews", { formatted: formatViews(ch.total_view_count) })
|
||||||
: null,
|
: null,
|
||||||
@@ -330,6 +353,27 @@ export default function ChannelPage({
|
|||||||
<SlidersHorizontal className="w-4 h-4" />
|
<SlidersHorizontal className="w-4 h-4" />
|
||||||
</button>
|
</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 && (
|
{!me.is_demo && (
|
||||||
<button
|
<button
|
||||||
onClick={() => block.mutate()}
|
onClick={() => block.mutate()}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"subscribers": "{{formatted}} subscribers",
|
"subscribers": "{{formatted}} subscribers",
|
||||||
"videoCount_one": "{{count}} video",
|
"videoCount_one": "{{count}} video",
|
||||||
"videoCount_other": "{{count}} videos",
|
"videoCount_other": "{{count}} videos",
|
||||||
|
"videoCountStored": "{{stored}} / {{total}} videos",
|
||||||
"totalViews": "{{formatted}} views",
|
"totalViews": "{{formatted}} views",
|
||||||
"joined": "Joined {{date}}",
|
"joined": "Joined {{date}}",
|
||||||
"loadingVideos": "Loading this channel's videos…",
|
"loadingVideos": "Loading this channel's videos…",
|
||||||
@@ -24,5 +25,9 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"topics": "Topics",
|
"topics": "Topics",
|
||||||
"keywords": "Keywords",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"subscribers": "{{formatted}} feliratkozó",
|
"subscribers": "{{formatted}} feliratkozó",
|
||||||
"videoCount_one": "{{count}} videó",
|
"videoCount_one": "{{count}} videó",
|
||||||
"videoCount_other": "{{count}} videó",
|
"videoCount_other": "{{count}} videó",
|
||||||
|
"videoCountStored": "{{stored}} / {{total}} videó",
|
||||||
"totalViews": "{{formatted}} megtekintés",
|
"totalViews": "{{formatted}} megtekintés",
|
||||||
"joined": "Csatlakozott: {{date}}",
|
"joined": "Csatlakozott: {{date}}",
|
||||||
"loadingVideos": "A csatorna videóinak betöltése…",
|
"loadingVideos": "A csatorna videóinak betöltése…",
|
||||||
@@ -24,5 +25,9 @@
|
|||||||
"language": "Nyelv",
|
"language": "Nyelv",
|
||||||
"topics": "Témák",
|
"topics": "Témák",
|
||||||
"keywords": "Kulcsszavak",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ export interface ChannelDetail {
|
|||||||
explored: boolean;
|
explored: boolean;
|
||||||
blocked: boolean;
|
blocked: boolean;
|
||||||
stored_videos: number;
|
stored_videos: number;
|
||||||
|
deep_requested: boolean;
|
||||||
|
backfill_done: boolean;
|
||||||
from_explore: boolean;
|
from_explore: boolean;
|
||||||
details_synced: boolean;
|
details_synced: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user