"""Klipy (feed Swipe): feed z afinicznością, pamięć widzianych, telemetria, ulubione, blacklista, onboarding. Logika bez bazy w `app/clips_feed.py`. **Feed** `GET /clips/feed`: strona = 80 % z puli afiniczności (tagi, nisze, twórcy z wag urządzenia) + 20 % explore (okno po kluczu głównym od kursora, losowanie ważone lajkami). Bez wag (nowe urządzenie, brak onboardingu) 100 % explore. Każdy podany klip ląduje w `clip_seen` od razu, więc nigdy nie wraca. Widoczność: żywy, do `clips_feed_max_duration_sec`, z plikiem, nie widziany, nie na blackliście (tag / nisza / twórca). **Zdarzenia** `POST /clips/events`: paczka z apki (co ~5 s). Zapis do `clip_feed_events`, podniesienie `clip_seen.outcome`, ulubione przy like/unlike, delty na wagi afiniczności (`clips_feed.apply_event`). Adresy plików i posterów są publiczne (`media.redgifs.com`), więc jadą do apki wprost, bez image proxy: RedGifs nie sprawdza Referera, a proxy tylko dodałoby VPS do toru. """ from __future__ import annotations import logging import time import uuid from datetime import UTC, datetime from fastapi import APIRouter, Depends, HTTPException, Query, Response from pydantic import BaseModel, Field from sqlalchemy import String, cast, func, select, text from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.orm import Session from app import clips_feed as cf from app.api.device import get_device_id from app.auth import require_api_key from app.config import get_settings from app.db import get_session from app.models.clip import Clip, ClipTag from app.models.clip_feed import ClipAffinity, ClipBlacklist, ClipFavorite, ClipFeedEvent, ClipSeen log = logging.getLogger(__name__) router = APIRouter(prefix="/clips", tags=["clips"], dependencies=[Depends(require_api_key)]) _EXPLORE_WINDOW = 2000 _AFFINITY_TAGS, _AFFINITY_NICHES, _AFFINITY_CREATORS = 6, 3, 3 _PER_TAG, _PER_NICHE, _PER_CREATOR = 60, 60, 40 _UUID_ZERO = uuid.UUID(int=0) _BLACKLIST_KINDS = frozenset({"tag", "niche", "creator"}) # ---- schematy --------------------------------------------------------------------------- class ClipOut(BaseModel): id: uuid.UUID source: str source_id: str title: str | None creator: str | None creator_verified: bool duration_sec: float | None width: int | None height: int | None has_audio: bool content_type: str | None niches: list[str] tags: list[str] hd_url: str | None sd_url: str | None poster_url: str | None thumb_url: str | None likes: int views: int published_at: datetime | None is_favorite: bool = False class ClipListOut(BaseModel): items: list[ClipOut] limit: int offset: int has_more: bool #: Liczba wszystkich klipow tworcy (tylko przy filtrze `creator`, indeks ix_clips_creator). #: Strona tworcy w apce pokazuje ja w naglowku; przy innych listach nie liczymy. total: int | None = None class ClipStatsOut(BaseModel): total: int alive: int dead: int with_audio: int verified: int by_source: dict[str, int] newest_published_at: datetime | None tags: int class FeedItemOut(BaseModel): clip: ClipOut #: `affinity` | `explore`, do telemetrii i do pomiaru, czy personalizacja coś daje. slot: str is_fresh: bool class FeedPageOut(BaseModel): items: list[FeedItemOut] next_cursor: str seed: str #: True gdy okno explore doszło do końca katalogu i zawinęło się na początek. wrapped: bool #: Ile klipów to urządzenie już widziało (pigułka „N nowych"). served_total: int personalized: bool class FeedEventIn(BaseModel): clip_id: uuid.UUID kind: str = Field(min_length=1, max_length=16) dwell_ms: int | None = Field(default=None, ge=0, le=3_600_000) reason: str | None = Field(default=None, max_length=160) slot: str | None = Field(default=None, max_length=12) position: int | None = Field(default=None, ge=0, le=32767) class FeedEventsIn(BaseModel): session_id: str | None = Field(default=None, max_length=32) events: list[FeedEventIn] = Field(max_length=100) class OnboardingIn(BaseModel): tags: list[str] = Field(default_factory=list, max_length=20) niches: list[str] = Field(default_factory=list, max_length=20) class OnboardingOptionOut(BaseModel): value: str count: int class OnboardingOptionsOut(BaseModel): tags: list[OnboardingOptionOut] niches: list[OnboardingOptionOut] class AffinityOut(BaseModel): tags: dict[str, float] niches: dict[str, float] creators: dict[str, float] events_seen: int class BlacklistItemOut(BaseModel): kind: str value: str # ---- pomocnicze ------------------------------------------------------------------------- def _tags_for(session: Session, clip_ids: list[uuid.UUID]) -> dict[uuid.UUID, list[str]]: if not clip_ids: return {} out: dict[uuid.UUID, list[str]] = {} for cid, tag in session.execute( select(ClipTag.clip_id, ClipTag.tag).where(ClipTag.clip_id.in_(clip_ids)).order_by(ClipTag.tag) ): out.setdefault(cid, []).append(tag) return out def _favorites_for(session: Session, device_id: str, clip_ids: list[uuid.UUID]) -> set[uuid.UUID]: if not clip_ids: return set() return { r[0] for r in session.execute( select(ClipFavorite.clip_id).where(ClipFavorite.device_id == device_id, ClipFavorite.clip_id.in_(clip_ids)) ) } def _clip_out(r: Clip, tags: list[str], fav: bool = False) -> ClipOut: return ClipOut( id=r.id, source=r.source, source_id=r.source_id, title=r.title, creator=r.creator, creator_verified=r.creator_verified, duration_sec=r.duration_sec, width=r.width, height=r.height, has_audio=r.has_audio, content_type=r.content_type, niches=list(r.niches or []), tags=tags, hd_url=r.hd_url, sd_url=r.sd_url, poster_url=r.poster_url, thumb_url=r.thumb_url, likes=r.likes, views=r.views, published_at=r.published_at, is_favorite=fav, ) def _load_affinity(session: Session, device_id: str) -> tuple[ClipAffinity | None, cf.Affinity]: row = session.get(ClipAffinity, device_id) if row is None: return None, cf.Affinity() return row, cf.Affinity( tags=dict(row.tag_weights or {}), niches=dict(row.niche_weights or {}), creators=dict(row.creator_weights or {}) ) def _save_affinity(session: Session, device_id: str, row: ClipAffinity | None, aff: cf.Affinity, events: int) -> None: if row is None: row = ClipAffinity(device_id=device_id) session.add(row) row.tag_weights = aff.tags row.niche_weights = aff.niches row.creator_weights = aff.creators row.events_seen = (row.events_seen or 0) + events def _blacklist(session: Session, device_id: str) -> dict[str, set[str]]: out: dict[str, set[str]] = {"tag": set(), "niche": set(), "creator": set()} for kind, value in session.execute( select(ClipBlacklist.kind, ClipBlacklist.value).where(ClipBlacklist.device_id == device_id) ): out.setdefault(kind, set()).add(value) return out def _visible_clauses(device_id: str, bl: dict[str, set[str]]) -> list: clauses = [ Clip.dead_at.is_(None), Clip.duration_sec.is_not(None), Clip.duration_sec <= get_settings().clips_feed_max_duration_sec, (Clip.hd_url.is_not(None)) | (Clip.sd_url.is_not(None)), ~select(ClipSeen.clip_id) .where(ClipSeen.device_id == device_id, ClipSeen.clip_id == Clip.id) .exists(), ] if bl["creator"]: clauses.append((Clip.creator.is_(None)) | (~Clip.creator.in_(list(bl["creator"])))) if bl["niche"]: clauses.append(~Clip.niches.overlap(list(bl["niche"]))) if bl["tag"]: clauses.append( ~select(ClipTag.clip_id) .where(ClipTag.clip_id == Clip.id, ClipTag.tag.in_(list(bl["tag"]))) .exists() ) return clauses _CAND_COLS = (Clip.id, Clip.creator, Clip.likes, Clip.published_at, Clip.niches) def _rows_to_candidates(session: Session, rows, *, with_tags: bool) -> list[cf.Candidate]: """Wiersze `_CAND_COLS` → Candidate. Tagi tylko na życzenie: pula explore punktuje wyłącznie lajkami, więc ciągnięcie tagów dla 300 kandydatów było czystym kosztem.""" tags = _tags_for(session, [r[0] for r in rows]) if with_tags else {} return [ cf.Candidate(id=r[0], creator=r[1], likes=r[2], published_at=r[3], tags=tags.get(r[0], []), niches=list(r[4] or [])) for r in rows ] def _candidates(session: Session, ids: list[uuid.UUID], clauses: list) -> list[cf.Candidate]: if not ids: return [] rows = session.execute(select(*_CAND_COLS).where(Clip.id.in_(ids), *clauses)).all() return _rows_to_candidates(session, rows, with_tags=True) def _explore_candidates( session: Session, pk: uuid.UUID, clauses: list, limit: int ) -> tuple[list[cf.Candidate], uuid.UUID, bool]: """Widoczni kandydaci z okna `_EXPLORE_WINDOW` id po `pk` (podzapytanie, bez przenoszenia 2000 uuid przez SQL). Zwraca (kandydaci, koniec okna, czy okno było ostatnie). Ostatnie okno = katalog wyczerpany od `pk`, następna strona zawija do zera.""" window = ( select(Clip.id).where(Clip.id > pk, Clip.dead_at.is_(None)).order_by(Clip.id).limit(_EXPLORE_WINDOW) ).subquery() # Postgres nie ma max(uuid); tekstowo porządek jest ten sam (małe hex, stały format). end_txt, n = session.execute( select(func.max(cast(window.c.id, String)), func.count()).select_from(window) ).one() if not n: return [], _UUID_ZERO, True end_id = uuid.UUID(end_txt) rows = session.execute( select(*_CAND_COLS).where(Clip.id.in_(select(window.c.id)), *clauses).order_by(Clip.id).limit(limit) ).all() last = n < _EXPLORE_WINDOW return _rows_to_candidates(session, rows, with_tags=False), (end_id if not last else _UUID_ZERO), last def _affinity_candidate_ids(session: Session, aff: cf.Affinity, rng) -> set[uuid.UUID]: ids: set[uuid.UUID] = set() for tag in aff.top("tags", _AFFINITY_TAGS): start = uuid.UUID(int=rng.getrandbits(128)) rows = session.execute( select(ClipTag.clip_id).where(ClipTag.tag == tag, ClipTag.clip_id > start).order_by(ClipTag.clip_id).limit(_PER_TAG) ).all() if len(rows) < _PER_TAG: rows += session.execute( select(ClipTag.clip_id).where(ClipTag.tag == tag).order_by(ClipTag.clip_id).limit(_PER_TAG - len(rows)) ).all() ids.update(r[0] for r in rows) for niche in aff.top("niches", _AFFINITY_NICHES): start = uuid.UUID(int=rng.getrandbits(128)) rows = session.execute( select(Clip.id).where(Clip.niches.overlap([niche]), Clip.id > start).order_by(Clip.id).limit(_PER_NICHE) ).all() ids.update(r[0] for r in rows) for creator in aff.top("creators", _AFFINITY_CREATORS): start = uuid.UUID(int=rng.getrandbits(128)) rows = session.execute( select(Clip.id).where(Clip.creator == creator, Clip.id > start).order_by(Clip.id).limit(_PER_CREATOR) ).all() if len(rows) < _PER_CREATOR: rows += session.execute( select(Clip.id).where(Clip.creator == creator).order_by(Clip.id).limit(_PER_CREATOR - len(rows)) ).all() ids.update(r[0] for r in rows) return ids # ---- feed ------------------------------------------------------------------------------- @router.get("/feed", response_model=FeedPageOut) def clip_feed( session: Session = Depends(get_session), device_id: str = Depends(get_device_id), seed: str | None = Query(default=None, max_length=16), cursor: str | None = Query(default=None, max_length=200), limit: int = Query(default=10, ge=1, le=30), ) -> FeedPageOut: t0 = time.monotonic() decoded = cf.decode_cursor(cursor) if cursor else None if decoded: seed, pk, page = decoded else: seed = (seed or cf.new_seed())[:16] pk, page = cf.seed_start_uuid(seed), 0 rng = cf.page_rng(seed, page) _row, aff = _load_affinity(session, device_id) personalized = not aff.is_empty() bl = _blacklist(session, device_id) clauses = _visible_clauses(device_id, bl) n_aff, n_explore = cf.split_counts(limit, personalized) picked_aff: list[cf.Candidate] = [] if n_aff: cand_ids = _affinity_candidate_ids(session, aff, rng) picked_aff = cf.pick_affinity(_candidates(session, list(cand_ids), clauses), aff, n_aff) taken = {c.id for c in picked_aff} # Explore: okno po PK; gdy w oknie brak widocznych, przesuwamy sie dalej (max 3 okna). # Braki z afinicznosci (za malo kandydatow po filtrach) dopelnia explore. picked_explore: list[cf.Candidate] = [] wrapped = False next_pk = pk want = n_explore + max(0, n_aff - len(picked_aff)) for _ in range(3): pool, next_pk, last = _explore_candidates(session, next_pk, clauses, limit=max(60, want * 15)) pool = [c for c in pool if c.id not in taken] picked_explore += cf.pick_explore(pool, want - len(picked_explore), rng) taken.update(c.id for c in picked_explore) if last: wrapped = True if len(picked_explore) >= want or last: break page_items = cf.compose_page(picked_aff, picked_explore, rng) clip_ids = [c.id for c, _ in page_items] if clip_ids: session.execute( pg_insert(ClipSeen) .values([{"device_id": device_id, "clip_id": cid, "outcome": "served"} for cid in clip_ids]) .on_conflict_do_nothing() ) rows = {r.id: r for r in session.execute(select(Clip).where(Clip.id.in_(clip_ids))).scalars()} if clip_ids else {} tags = _tags_for(session, clip_ids) favs = _favorites_for(session, device_id, clip_ids) served_total = session.execute( select(func.count()).select_from(ClipSeen).where(ClipSeen.device_id == device_id) ).scalar_one() session.commit() items = [ FeedItemOut( clip=_clip_out(rows[c.id], tags.get(c.id, []), c.id in favs), slot=slot, is_fresh=cf.is_fresh(rows[c.id].published_at), ) for c, slot in page_items if c.id in rows ] log.info( "clips/feed: device=%s page=%d served=%d aff=%d explore=%d wrapped=%s ms=%d", device_id[:8], page, len(items), len(picked_aff), len(picked_explore), wrapped, int((time.monotonic() - t0) * 1000), ) return FeedPageOut( items=items, next_cursor=cf.encode_cursor(seed, next_pk, page + 1), seed=seed, wrapped=wrapped, served_total=int(served_total), personalized=personalized, ) # ---- zdarzenia -------------------------------------------------------------------------- @router.post("/events", status_code=204) def clip_events( body: FeedEventsIn, session: Session = Depends(get_session), device_id: str = Depends(get_device_id), ) -> Response: events = [e for e in body.events if e.kind in cf.ALLOWED_KINDS] if not events: return Response(status_code=204) clip_ids = list({e.clip_id for e in events}) clips = {r.id: r for r in session.execute(select(Clip).where(Clip.id.in_(clip_ids))).scalars()} tags = _tags_for(session, clip_ids) seen = { r.clip_id: r for r in session.execute( select(ClipSeen).where(ClipSeen.device_id == device_id, ClipSeen.clip_id.in_(clip_ids)) ).scalars() } row, aff = _load_affinity(session, device_id) changed = 0 for e in events: session.add( ClipFeedEvent( device_id=device_id, session_id=body.session_id, clip_id=e.clip_id, kind=e.kind, dwell_ms=e.dwell_ms, reason=e.reason, slot=e.slot, position=e.position, ) ) clip = clips.get(e.clip_id) if clip is None: continue # clip_seen: podnies wynik, nigdy nie cofaj. outcome = e.kind if e.kind in cf.OUTCOME_RANK else None if outcome: s = seen.get(e.clip_id) if s is None: s = ClipSeen(device_id=device_id, clip_id=e.clip_id, outcome=outcome) session.add(s) seen[e.clip_id] = s elif cf.OUTCOME_RANK[outcome] > cf.OUTCOME_RANK.get(s.outcome, 0): s.outcome = outcome if e.kind == "like": session.execute( pg_insert(ClipFavorite).values(device_id=device_id, clip_id=e.clip_id).on_conflict_do_nothing() ) elif e.kind == "unlike": session.execute( text("DELETE FROM clip_favorites WHERE device_id = :d AND clip_id = :c"), {"d": device_id, "c": e.clip_id}, ) if cf.apply_event( aff, kind=e.kind, dwell_ms=e.dwell_ms, reason=e.reason, tags=tags.get(e.clip_id, []), niches=list(clip.niches or []), creator=clip.creator, ): changed += 1 if changed: _save_affinity(session, device_id, row, aff, changed) session.commit() return Response(status_code=204) # ---- onboarding, afinicznosc ------------------------------------------------------------ _OPTIONS_CACHE: dict[str, tuple[float, OnboardingOptionsOut]] = {} @router.get("/onboarding-options", response_model=OnboardingOptionsOut) def onboarding_options( session: Session = Depends(get_session), q: str | None = Query(default=None, max_length=64, description="Szukaj w slowniku tagow"), ) -> OnboardingOptionsOut: """Najpopularniejsze tagi i nisze do wyboru przy pierwszym wejsciu w Edge. Liczone z bazy, cache 10 min w procesie (szukanie z pominieciem cache). `q` szuka po calym slowniku (1,7 tys. tagow), bo sama czolowka nie wystarcza: „anal" to 27. tag co do liczby klipow i wypadal poza liste (uwaga Jana 2026-09-16).""" if q and q.strip(): needle = f"%{q.strip().lower()}%" found = [ OnboardingOptionOut(value=t, count=n) for t, n in session.execute( select(ClipTag.tag, func.count()) .where(ClipTag.tag.like(needle)) .group_by(ClipTag.tag) .order_by(func.count().desc()) .limit(30) ) ] return OnboardingOptionsOut(tags=found, niches=[]) hit = _OPTIONS_CACHE.get("v1") if hit and time.monotonic() - hit[0] < 600: return hit[1] tags = [ OnboardingOptionOut(value=t, count=n) for t, n in session.execute( select(ClipTag.tag, func.count()).group_by(ClipTag.tag).order_by(func.count().desc()).limit(60) ) ] niches = [ OnboardingOptionOut(value=v, count=n) for v, n in session.execute( text("SELECT n, count(*) FROM clips, unnest(niches) n WHERE dead_at IS NULL GROUP BY n ORDER BY 2 DESC LIMIT 30") ) ] out = OnboardingOptionsOut(tags=tags, niches=niches) _OPTIONS_CACHE["v1"] = (time.monotonic(), out) return out @router.post("/onboarding", response_model=AffinityOut) def onboarding( body: OnboardingIn, session: Session = Depends(get_session), device_id: str = Depends(get_device_id), ) -> AffinityOut: # +8 za wskazany tag, wiecej niz polubienie klipu (+5): to jawna deklaracja, a feed # inaczej zbyt dlugo dochodzil do tego, co user i tak powiedzial na starcie (uwaga # Jana 2026-09-16). Ten sam endpoint sluzy pozniejszemu dostrajaniu z zakladki Edge, # wiec kolejne wskazania dokladaja sie do wag. row, aff = _load_affinity(session, device_id) for t in body.tags: t = t.strip().lower()[:64] if t: aff.tags[t] = round(aff.tags.get(t, 0.0) + 8.0, 3) for n in body.niches: n = n.strip()[:64] if n: aff.niches[n] = round(aff.niches.get(n, 0.0) + 8.0, 3) _save_affinity(session, device_id, row, aff, 0) session.commit() row2 = session.get(ClipAffinity, device_id) return AffinityOut(tags=aff.tags, niches=aff.niches, creators=aff.creators, events_seen=row2.events_seen if row2 else 0) @router.get("/affinity", response_model=AffinityOut) def get_affinity(session: Session = Depends(get_session), device_id: str = Depends(get_device_id)) -> AffinityOut: row, aff = _load_affinity(session, device_id) return AffinityOut(tags=aff.tags, niches=aff.niches, creators=aff.creators, events_seen=row.events_seen if row else 0) @router.delete("/affinity", status_code=204) def reset_affinity( session: Session = Depends(get_session), device_id: str = Depends(get_device_id), seen: bool = Query(default=False, description="True = zapomnij tez widziane klipy (feed od zera)"), ) -> Response: """„Resetuj moj feed": kasuje wagi; z `seen=true` takze pamiec widzianych.""" session.execute(text("DELETE FROM clip_affinity WHERE device_id = :d"), {"d": device_id}) if seen: session.execute(text("DELETE FROM clip_seen WHERE device_id = :d"), {"d": device_id}) session.commit() return Response(status_code=204) # ---- ulubione, blacklista ---------------------------------------------------------------- @router.get("/favorites", response_model=ClipListOut) def clip_favorites( session: Session = Depends(get_session), device_id: str = Depends(get_device_id), limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), ) -> ClipListOut: rows = session.execute( select(Clip) .join(ClipFavorite, ClipFavorite.clip_id == Clip.id) .where(ClipFavorite.device_id == device_id) .order_by(ClipFavorite.created_at.desc()) .offset(offset) .limit(limit + 1) ).scalars().all() has_more = len(rows) > limit rows = rows[:limit] tags = _tags_for(session, [r.id for r in rows]) return ClipListOut( items=[_clip_out(r, tags.get(r.id, []), True) for r in rows], limit=limit, offset=offset, has_more=has_more ) @router.get("/blacklist", response_model=list[BlacklistItemOut]) def clip_blacklist(session: Session = Depends(get_session), device_id: str = Depends(get_device_id)) -> list[BlacklistItemOut]: return [ BlacklistItemOut(kind=k, value=v) for k, v in session.execute( select(ClipBlacklist.kind, ClipBlacklist.value).where(ClipBlacklist.device_id == device_id).order_by(ClipBlacklist.kind, ClipBlacklist.value) ) ] @router.post("/blacklist/{kind}/{value}", status_code=204) def clip_blacklist_add( kind: str, value: str, session: Session = Depends(get_session), device_id: str = Depends(get_device_id) ) -> Response: if kind not in _BLACKLIST_KINDS or not value.strip(): raise HTTPException(status_code=400, detail="kind must be tag|niche|creator") session.execute( pg_insert(ClipBlacklist).values(device_id=device_id, kind=kind, value=value.strip()[:128]).on_conflict_do_nothing() ) session.commit() return Response(status_code=204) @router.delete("/blacklist/{kind}/{value}", status_code=204) def clip_blacklist_remove( kind: str, value: str, session: Session = Depends(get_session), device_id: str = Depends(get_device_id) ) -> Response: session.execute( text("DELETE FROM clip_blacklist WHERE device_id = :d AND kind = :k AND value = :v"), {"d": device_id, "k": kind, "v": value.strip()[:128]}, ) session.commit() return Response(status_code=204) # ---- listing i statystyki (pilot) -------------------------------------------------------- @router.get("", response_model=ClipListOut) def list_clips( session: Session = Depends(get_session), device_id: str = Depends(get_device_id), tag: str | None = Query(default=None, max_length=64), source: str | None = Query(default=None, max_length=16), creator: str | None = Query(default=None, max_length=128), has_audio: bool | None = None, max_duration_sec: float | None = Query(default=None, gt=0), limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), ) -> ClipListOut: q = select(Clip).where(Clip.dead_at.is_(None)) if tag: q = q.where(Clip.id.in_(select(ClipTag.clip_id).where(ClipTag.tag == tag))) if source: q = q.where(Clip.source == source) if creator: q = q.where(Clip.creator == creator) if has_audio is not None: q = q.where(Clip.has_audio.is_(has_audio)) if max_duration_sec is not None: q = q.where(Clip.duration_sec <= max_duration_sec) rows = session.execute( q.order_by(Clip.published_at.desc().nullslast(), Clip.id).offset(offset).limit(limit + 1) ).scalars().all() has_more = len(rows) > limit rows = rows[:limit] ids = [r.id for r in rows] tags = _tags_for(session, ids) favs = _favorites_for(session, device_id, ids) total = None if creator: total = session.execute( select(func.count()).select_from(Clip).where(Clip.dead_at.is_(None), Clip.creator == creator) ).scalar_one() return ClipListOut( items=[_clip_out(r, tags.get(r.id, []), r.id in favs) for r in rows], limit=limit, offset=offset, has_more=has_more, total=total, ) @router.get("/stats", response_model=ClipStatsOut) def clip_stats(session: Session = Depends(get_session)) -> ClipStatsOut: row = session.execute( select( func.count(), func.count().filter(Clip.dead_at.is_(None)), func.count().filter(Clip.dead_at.is_not(None)), func.count().filter(Clip.has_audio.is_(True), Clip.dead_at.is_(None)), func.count().filter(Clip.creator_verified.is_(True), Clip.dead_at.is_(None)), func.max(Clip.published_at), ) ).one() by_source = {s: n for s, n in session.execute(select(Clip.source, func.count()).group_by(Clip.source))} tags = session.execute(select(func.count(func.distinct(ClipTag.tag)))).scalar_one() return ClipStatsOut( total=row[0], alive=row[1], dead=row[2], with_audio=row[3], verified=row[4], by_source=by_source, newest_published_at=row[5], tags=tags, )