feat(post-api): get_post returns uncapped thumbnails — PostModal masonry needs full image list; feed query unchanged (still capped at 6 for previews). _thumbnails_for gains a limit kwarg; get_post passes limit=None.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -109,7 +109,10 @@ class PostFeedService:
|
|||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
post, artist, source = row
|
post, artist, source = row
|
||||||
thumbs_map = await self._thumbnails_for([post.id])
|
# Detail endpoint returns the FULL image list for PostModal's
|
||||||
|
# masonry grid — feed query still caps at THUMBNAIL_LIMIT via
|
||||||
|
# the default arg.
|
||||||
|
thumbs_map = await self._thumbnails_for([post.id], limit=None)
|
||||||
atts_map = await self._attachments_for([post.id])
|
atts_map = await self._attachments_for([post.id])
|
||||||
item = self._to_dict(post, artist, source, thumbs_map, atts_map)
|
item = self._to_dict(post, artist, source, thumbs_map, atts_map)
|
||||||
item["description_full"] = html_to_plain(post.description)
|
item["description_full"] = html_to_plain(post.description)
|
||||||
@@ -117,15 +120,21 @@ class PostFeedService:
|
|||||||
|
|
||||||
# --- composition helpers ---------------------------------------------
|
# --- composition helpers ---------------------------------------------
|
||||||
|
|
||||||
async def _thumbnails_for(self, post_ids: list[int]) -> dict[int, dict]:
|
async def _thumbnails_for(
|
||||||
"""post_id -> {"thumbs": [...up to 6], "more": int}.
|
self, post_ids: list[int], *, limit: int | None = THUMBNAIL_LIMIT,
|
||||||
|
) -> dict[int, dict]:
|
||||||
|
"""post_id -> {"thumbs": [...up to limit], "more": int}.
|
||||||
|
|
||||||
Selects THUMBNAIL_LIMIT+1 images per post via window function so we
|
Selects up to `limit` images per post via window function so we
|
||||||
can detect overflow in a single query.
|
can detect overflow in a single query. Pass `limit=None` to
|
||||||
|
return ALL thumbnails per post (used by `get_post` for PostModal's
|
||||||
|
masonry grid; the feed pass keeps the default cap so payloads
|
||||||
|
stay small).
|
||||||
"""
|
"""
|
||||||
if not post_ids:
|
if not post_ids:
|
||||||
return {}
|
return {}
|
||||||
# Rank images within each post and fetch only the top THUMBNAIL_LIMIT+1.
|
# Rank images within each post; cap at `limit` rows per post when
|
||||||
|
# limit is set, return all when limit is None.
|
||||||
ranked = (
|
ranked = (
|
||||||
select(
|
select(
|
||||||
ImageRecord.id,
|
ImageRecord.id,
|
||||||
@@ -143,12 +152,13 @@ class PostFeedService:
|
|||||||
.where(ImageRecord.primary_post_id.in_(post_ids))
|
.where(ImageRecord.primary_post_id.in_(post_ids))
|
||||||
.subquery()
|
.subquery()
|
||||||
)
|
)
|
||||||
rows = (await self.session.execute(
|
stmt = select(
|
||||||
select(
|
ranked.c.id, ranked.c.primary_post_id,
|
||||||
ranked.c.id, ranked.c.primary_post_id,
|
ranked.c.sha256, ranked.c.mime, ranked.c.total,
|
||||||
ranked.c.sha256, ranked.c.mime, ranked.c.total,
|
)
|
||||||
).where(ranked.c.rn <= THUMBNAIL_LIMIT)
|
if limit is not None:
|
||||||
)).all()
|
stmt = stmt.where(ranked.c.rn <= limit)
|
||||||
|
rows = (await self.session.execute(stmt)).all()
|
||||||
|
|
||||||
out: dict[int, dict] = {pid: {"thumbs": [], "more": 0} for pid in post_ids}
|
out: dict[int, dict] = {pid: {"thumbs": [], "more": 0} for pid in post_ids}
|
||||||
for img_id, pid, sha, mime, total in rows:
|
for img_id, pid, sha, mime, total in rows:
|
||||||
|
|||||||
@@ -117,3 +117,50 @@ async def test_detail_404_for_unknown(client):
|
|||||||
assert resp.status_code == 404
|
assert resp.status_code == 404
|
||||||
body = await resp.get_json()
|
body = await resp.get_json()
|
||||||
assert body["error"] == "not_found"
|
assert body["error"] == "not_found"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_detail_returns_uncapped_thumbnails(client, db):
|
||||||
|
"""Feed query caps thumbnails at 6 for previews; detail endpoint
|
||||||
|
returns the full list so PostModal can render the masonry grid."""
|
||||||
|
from backend.app.models import ImageRecord
|
||||||
|
|
||||||
|
a = Artist(name="yuki-api", slug="yuki-api")
|
||||||
|
db.add(a)
|
||||||
|
await db.flush()
|
||||||
|
s = Source(
|
||||||
|
artist_id=a.id, platform="patreon",
|
||||||
|
url="https://patreon.com/cw/yuki-api", enabled=True,
|
||||||
|
)
|
||||||
|
db.add(s)
|
||||||
|
await db.flush()
|
||||||
|
p = Post(
|
||||||
|
source_id=s.id, external_post_id="DETAIL10",
|
||||||
|
post_title="big post", description="<p>body</p>",
|
||||||
|
)
|
||||||
|
db.add(p)
|
||||||
|
await db.flush()
|
||||||
|
# Seed 10 ImageRecord rows linked to this post via primary_post_id.
|
||||||
|
for i in range(10):
|
||||||
|
sha = f"y{i:x}".ljust(64, "0")[:64]
|
||||||
|
rec = ImageRecord(
|
||||||
|
path=f"/images/test-yuki-{i}.jpg",
|
||||||
|
sha256=sha,
|
||||||
|
size_bytes=1,
|
||||||
|
mime="image/jpeg",
|
||||||
|
width=64,
|
||||||
|
height=64,
|
||||||
|
origin="downloaded",
|
||||||
|
integrity_status="unknown",
|
||||||
|
primary_post_id=p.id,
|
||||||
|
artist_id=a.id,
|
||||||
|
)
|
||||||
|
db.add(rec)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/posts/{p.id}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = await resp.get_json()
|
||||||
|
# Detail returns ALL 10 thumbnails (feed would return 6 + thumbnails_more).
|
||||||
|
assert len(body["thumbnails"]) == 10
|
||||||
|
assert body["description_full"] == "body"
|
||||||
|
|||||||
Reference in New Issue
Block a user