feat(m5d): demand-driven deep backfill + per-user ETA
Per-user opt-in to full-history (deep) backfill so a new user's unique channels no longer trigger a big shared-quota burst. - migration 0007: Subscription.deep_requested (default false); seed admins' existing subscriptions to preserve today's global-backfill behaviour - run_deep_backfill is now demand-driven: only channels at least one user has requested are deep-backfilled; recent backfill stays unconditional (cheap) - estimate_deep_backfill ETA helper (quota-bound) surfaced in /api/sync/my-status - POST /api/sync/deep-all to opt all my channels in; PATCH channels accepts deep_requested - UI: per-channel Full history toggle, Backfill everything action, deep progress + ETA in Channels header and Settings - Sync
This commit is contained in:
parent
bc8db45323
commit
5f60b3e9fe
9 changed files with 256 additions and 18 deletions
|
|
@ -5,12 +5,14 @@ import {
|
|||
ArrowUp,
|
||||
Eye,
|
||||
EyeOff,
|
||||
History,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Search,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { api, type ManagedChannel, type Tag } from "../lib/api";
|
||||
import { formatEta } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
|
|
@ -35,8 +37,10 @@ export default function Channels({
|
|||
const userTags = (tagsQuery.data ?? []).filter((t) => !t.system);
|
||||
|
||||
const patch = useMutation({
|
||||
mutationFn: (v: { id: string; body: { priority?: number; hidden?: boolean } }) =>
|
||||
api.updateChannel(v.id, v.body),
|
||||
mutationFn: (v: {
|
||||
id: string;
|
||||
body: { priority?: number; hidden?: boolean; deep_requested?: boolean };
|
||||
}) => api.updateChannel(v.id, v.body),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["channels"] }),
|
||||
});
|
||||
const attach = useMutation({
|
||||
|
|
@ -66,6 +70,18 @@ export default function Channels({
|
|||
mutationFn: (id: number) => api.deleteTag(id),
|
||||
onSuccess: () => invalidate(),
|
||||
});
|
||||
const deepAll = useMutation({
|
||||
mutationFn: () => api.deepAll(true),
|
||||
onSuccess: (r: { updated?: number }) => {
|
||||
qc.invalidateQueries({ queryKey: ["channels"] });
|
||||
qc.invalidateQueries({ queryKey: ["my-status"] });
|
||||
notify({
|
||||
level: "success",
|
||||
message: `Full history requested for ${r.updated ?? 0} channels`,
|
||||
});
|
||||
},
|
||||
onError: () => notify({ level: "error", message: "Couldn't request full history" }),
|
||||
});
|
||||
|
||||
const channels = (channelsQuery.data ?? []).filter(
|
||||
(c) => !q || (c.title ?? "").toLowerCase().includes(q.toLowerCase())
|
||||
|
|
@ -85,9 +101,16 @@ export default function Channels({
|
|||
/>
|
||||
<Stat
|
||||
label="Full history"
|
||||
value={`${s.channels_deep_done}/${s.channels_total}`}
|
||||
hint="Channels whose entire back-catalog is fetched. The rest backfill gradually."
|
||||
value={`${s.channels_deep_done}/${s.channels_deep_requested}`}
|
||||
hint="Channels whose entire back-catalog is fetched, out of the ones you've requested full history for."
|
||||
/>
|
||||
{s.deep_pending_count > 0 && (
|
||||
<Stat
|
||||
label="left"
|
||||
value={formatEta(s.deep_eta_seconds)}
|
||||
hint={`Rough estimate to finish full-history backfill of ${s.deep_pending_count} pending channel(s), based on the shared daily quota.`}
|
||||
/>
|
||||
)}
|
||||
<Stat label="My videos" value={s.my_videos.toLocaleString()} hint="Total videos available across your channels." />
|
||||
<Stat
|
||||
label="Quota left"
|
||||
|
|
@ -121,6 +144,19 @@ export default function Channels({
|
|||
Sync subscriptions
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
hint="Request full back-catalog backfill for every channel you're subscribed to. Older videos and search become complete as the shared daily quota allows — this can take a while."
|
||||
>
|
||||
<button
|
||||
onClick={() => deepAll.mutate()}
|
||||
disabled={deepAll.isPending}
|
||||
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
|
||||
>
|
||||
<History className={`w-4 h-4 ${deepAll.isPending ? "animate-pulse" : ""}`} />
|
||||
Backfill everything
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted mb-4 leading-relaxed">
|
||||
|
|
@ -181,6 +217,9 @@ export default function Channels({
|
|||
onView={() => onViewChannel(c.id, c.title ?? "This channel")}
|
||||
onPriority={(d) => patch.mutate({ id: c.id, body: { priority: c.priority + d } })}
|
||||
onHide={() => patch.mutate({ id: c.id, body: { hidden: !c.hidden } })}
|
||||
onDeep={() =>
|
||||
patch.mutate({ id: c.id, body: { deep_requested: !c.deep_requested } })
|
||||
}
|
||||
onToggleTag={(tagId) =>
|
||||
c.tag_ids.includes(tagId)
|
||||
? detach.mutate({ id: c.id, tagId })
|
||||
|
|
@ -232,6 +271,7 @@ function ChannelRow({
|
|||
onView,
|
||||
onPriority,
|
||||
onHide,
|
||||
onDeep,
|
||||
onToggleTag,
|
||||
}: {
|
||||
c: ManagedChannel;
|
||||
|
|
@ -239,6 +279,7 @@ function ChannelRow({
|
|||
onView: () => void;
|
||||
onPriority: (delta: number) => void;
|
||||
onHide: () => void;
|
||||
onDeep: () => void;
|
||||
onToggleTag: (tagId: number) => void;
|
||||
}) {
|
||||
return (
|
||||
|
|
@ -279,11 +320,29 @@ function ChannelRow({
|
|||
label="recent"
|
||||
hint={c.recent_synced ? "Recent uploads synced." : "Recent uploads not fetched yet."}
|
||||
/>
|
||||
<SyncBadge
|
||||
ok={c.backfill_done}
|
||||
label="full"
|
||||
hint={c.backfill_done ? "Full history fetched." : "Only recent videos so far; full history pending."}
|
||||
/>
|
||||
{c.backfill_done ? (
|
||||
<SyncBadge ok label="full" hint="Full history fetched." />
|
||||
) : (
|
||||
<Tooltip
|
||||
hint={
|
||||
c.deep_requested
|
||||
? "Full history requested — this channel's whole back-catalog will backfill as the shared quota allows. Click to cancel the request."
|
||||
: "Only recent uploads so far. Click to request this channel's full back-catalog (older videos + complete search)."
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={onDeep}
|
||||
className={`inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full border transition ${
|
||||
c.deep_requested
|
||||
? "bg-accent text-accent-fg border-accent"
|
||||
: "bg-card border-border text-muted hover:border-accent"
|
||||
}`}
|
||||
>
|
||||
<History className="w-3 h-3" />
|
||||
{c.deep_requested ? "full history queued" : "get full history"}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{userTags.map((t) => {
|
||||
const on = c.tag_ids.includes(t.id);
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue