refactor(catalog): the HTTP layer reads through queries and writes through the service

Channels, search and sync do no database work in their routes any more: reads go through
catalog/queries, writes through catalog/service, and a live search's ingest is catalog/search.
What a catalog route needs from Reader (a playlist's channels, a user's tag links, the feed
projection of results) it asks reader/queries and reader/service. The scheduler and the runs page
read channel titles and the work queue through catalog/queries. Catalog is fenced below its HTTP
layer: its owner filters go through userscope, it reads no other unit's tables, and its ingest
jobs commit only where they are named.
This commit is contained in:
2026-10-19 02:32:20 +02:00
parent 238e6f8a61
commit 5229fd96be
21 changed files with 1223 additions and 726 deletions
+5 -5
View File
@@ -16,7 +16,7 @@ from datetime import datetime, timezone
from app import sysconfig
from app.models import User
from app.catalog.models import Video
from app.routes.search import _ingest_candidates
from app.catalog.search import ingest_candidates
class _FakeYT:
@@ -71,7 +71,7 @@ def test_scrape_ingest_dateless_stub_ingests_and_takes_real_enrich_date(db):
"vid_bbbbbbb": _enrich_item("vid_bbbbbbb", "2023-10-30T17:00:08Z"),
}
kept = _ingest_candidates(db, _FakeYT(enrich), user, candidates)
kept = ingest_candidates(db, _FakeYT(enrich), user, candidates)
# No IntegrityError, and both non-Short/non-live videos are kept in input order.
assert kept == ["vid_aaaaaaa", "vid_bbbbbbb"]
@@ -89,7 +89,7 @@ def test_scrape_ingest_enrich_miss_keeps_nonnull_placeholder(db):
db.add(user)
db.commit()
kept = _ingest_candidates(db, _FakeYT({}), user, [_candidate("vid_ccccccc", "UC_chan_c")])
kept = ingest_candidates(db, _FakeYT({}), user, [_candidate("vid_ccccccc", "UC_chan_c")])
assert kept == []
c = db.get(Video, "vid_ccccccc")
@@ -101,7 +101,7 @@ def test_search_youtube_route_returns_422_when_ingest_fails(db, monkeypatch, ove
# Route-level guard (B-core-1): an ingest failure with nothing yet collected must surface as a 422
# + message on this JSON endpoint (the FE hides 5xx behind a generic "connection lost" + auto-retry,
# burning a second quota-costed search), and the session is rolled back. Here the page fetch succeeds
# (one candidate) and _ingest_candidates raises → the endpoint returns 422.
# (one candidate) and ingest_candidates raises → the endpoint returns 422.
from fastapi.testclient import TestClient
import app.state as state
@@ -144,7 +144,7 @@ def test_search_youtube_route_returns_422_when_ingest_fails(db, monkeypatch, ove
def _boom(db_, yt, user_, candidates):
raise RuntimeError("ingest exploded")
monkeypatch.setattr(search_mod, "_ingest_candidates", _boom)
monkeypatch.setattr(search_mod.catalog_search, "ingest_candidates", _boom)
override_dep(get_db, db.info["request"])
override_dep(current_user, lambda: user)