feat(channels): aggregate columns + smarter tag display

- Add Last upload, total Length and a Normal/Shorts/Live breakdown, computed on read in the
  existing grouped query (~35ms over the full catalog — measured, so no denormalization).
- Subscribers shown compact (919K), since the YouTube API already rounds them.
- Tag cell now shows only the tags actually attached to a channel; a '+' opens a per-channel
  picker to attach/detach, instead of listing every user tag on every row.
This commit is contained in:
npeter83 2026-06-17 23:24:25 +02:00
parent 14da25bf19
commit 0aa16543d9
6 changed files with 153 additions and 30 deletions

View file

@ -27,16 +27,33 @@ def list_channels(
.order_by(Subscription.priority.desc(), func.lower(Channel.title))
).all()
# Per-channel stored video count (shared data) in one grouped query.
# Per-channel aggregates over the shared catalog in ONE grouped pass: stored count, last
# upload, total duration, and a Normal/Shorts/Live breakdown. Cheap — a single grouped scan
# (~35 ms over the full 233k-row catalog on the dev DB), so computed on read rather than
# denormalised (measure-first: reads aren't frequent/hot enough to warrant cached columns).
channel_ids = [c.id for _, c in rows]
stored: dict[str, int] = {}
agg: dict[str, dict] = {}
if channel_ids:
for cid, count in db.execute(
select(Video.channel_id, func.count(Video.id))
for cid, n, last_up, total_dur, shorts, live in db.execute(
select(
Video.channel_id,
func.count(Video.id),
func.max(Video.published_at),
func.coalesce(func.sum(Video.duration_seconds), 0),
func.count(Video.id).filter(Video.is_short.is_(True)),
func.count(Video.id).filter(Video.live_status.in_(("live", "upcoming"))),
)
.where(Video.channel_id.in_(channel_ids))
.group_by(Video.channel_id)
).all():
stored[cid] = count
agg[cid] = {
"stored": n,
"last_video_at": last_up.isoformat() if last_up else None,
"total_duration_seconds": int(total_dur or 0),
"count_short": int(shorts),
"count_live": int(live),
"count_normal": max(0, int(n) - int(shorts) - int(live)),
}
# Channels already in the *global* deep-backfill queue — i.e. at least one user (any
# user) has requested full history and it isn't done yet. Their whole back-catalogue is
@ -71,7 +88,12 @@ def list_channels(
"thumbnail_url": ch.thumbnail_url,
"subscriber_count": ch.subscriber_count,
"video_count": ch.video_count,
"stored_videos": stored.get(ch.id, 0),
"stored_videos": (agg.get(ch.id) or {}).get("stored", 0),
"last_video_at": (agg.get(ch.id) or {}).get("last_video_at"),
"total_duration_seconds": (agg.get(ch.id) or {}).get("total_duration_seconds", 0),
"count_normal": (agg.get(ch.id) or {}).get("count_normal", 0),
"count_short": (agg.get(ch.id) or {}).get("count_short", 0),
"count_live": (agg.get(ch.id) or {}).get("count_live", 0),
"priority": sub.priority,
"hidden": sub.hidden,
"deep_requested": sub.deep_requested,