feat: a teaser's card references the drop it announced and the piece's variants (4402, 4401)
The operator's problem: a Patreon teaser is a pointer, and its card showed the censored crop plus a text link while the content it pointed at sat on another card. The fix is a REFERENCE, not an absorption: "the nested items on the unified post are a duplicate or reference of existing content". Nothing is written. Discord posts keep their own rows, dates and places in the feed. - post_unification: for each teaser with a linked association, the drop's images and text, plus its variant family: Discord images sharing the seed's gated LEADING working name, or a phash near-duplicate, within a window of the teaser. One hop only, oldest first. - Measured on artist 8 before writing it: of 121 message pairs 2-60 days apart that share a gated token, 106 share the leading name and all read as real families. Of the 15 sharing only a trailing word, 14 are sibling pieces and one is a plain collision (`bottom`, 56 days). The family cap is 8, not the pairing cap of 6, because `tentacooler` and `0-k1` (6 posts each) are real families. - The feed drops a linked drop's own card only within discord_link_fold_hours of its teaser (default 24): "only hidden from the post view they're posted the same day". Older referenced posts stay where they landed. - post_association.linked_by records whether FC or a person made the link, so the card can say so. Undo is the existing dismiss. - `image0` (gallery-dl's fallback name) becomes a stopword. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
@@ -19,6 +19,7 @@ from ..models import (
|
||||
ExternalLink,
|
||||
ImageProvenance,
|
||||
ImageRecord,
|
||||
ImportSettings,
|
||||
Post,
|
||||
PostAttachment,
|
||||
Source,
|
||||
@@ -118,6 +119,15 @@ class PostFeedService:
|
||||
# from. `around` and `get_post` deliberately do NOT apply this: reaching
|
||||
# a member by id is how you inspect a grouping.
|
||||
stmt = stmt.where(Post.absorbed_by_post_id.is_(None))
|
||||
# A linked Discord drop is shown ON its teaser's card by reference
|
||||
# (#4402), so its own card sitting beside that teaser is the same
|
||||
# release twice. Only then is it left out — an older drop keeps its
|
||||
# place, because a reference does not take anything out of history.
|
||||
fold_hours = await self._fold_hours()
|
||||
if fold_hours > 0:
|
||||
from .post_unification import fold_clause
|
||||
|
||||
stmt = stmt.where(fold_clause(fold_hours))
|
||||
if artist_id is not None:
|
||||
stmt = stmt.where(Post.artist_id == artist_id)
|
||||
if platform is not None:
|
||||
@@ -169,9 +179,12 @@ class PostFeedService:
|
||||
thumbs_map = await self._thumbnails_for(post_ids)
|
||||
atts_map = await self._attachments_for(post_ids)
|
||||
links_map = await self._links_for(post_ids)
|
||||
unified_map = await self._unified_for([p for p, _, _ in rows])
|
||||
|
||||
items = [
|
||||
self._to_dict(post, artist, source, thumbs_map, atts_map, links_map)
|
||||
self._to_dict(
|
||||
post, artist, source, thumbs_map, atts_map, links_map, unified_map,
|
||||
)
|
||||
for post, artist, source in rows
|
||||
]
|
||||
return {"items": items, "next_cursor": next_cursor}
|
||||
@@ -213,6 +226,7 @@ class PostFeedService:
|
||||
anchor_item = self._to_dict(
|
||||
anchor_post, anchor_artist, anchor_source, thumbs_map, atts_map,
|
||||
await self._links_for([anchor_post.id]),
|
||||
await self._unified_for([anchor_post]),
|
||||
)
|
||||
return {
|
||||
"items": newer["items"] + [anchor_item] + older["items"],
|
||||
@@ -239,6 +253,7 @@ class PostFeedService:
|
||||
item = self._to_dict(
|
||||
post, artist, source, thumbs_map, atts_map,
|
||||
await self._links_for([post.id]),
|
||||
await self._unified_for([post]),
|
||||
)
|
||||
item["description_full"] = html_to_plain(post.description)
|
||||
# Full (uncapped) translated description for the detail view (#143).
|
||||
@@ -385,9 +400,27 @@ class PostFeedService:
|
||||
|
||||
return await PostAssociationService(self.session).linked_for(post_ids)
|
||||
|
||||
async def _unified_for(self, posts: list[Post]) -> dict[int, dict]:
|
||||
"""The reference set each teaser's card shows (#4402). Local import for
|
||||
the same reason as `_links_for`: it reaches the association service."""
|
||||
from .post_unification import PostUnificationService
|
||||
|
||||
return await PostUnificationService(self.session).unified_for(posts)
|
||||
|
||||
async def _fold_hours(self) -> float:
|
||||
"""`discord_link_fold_hours`, read without assuming the row exists.
|
||||
|
||||
The feed is the one surface that must not fail on a settings row the
|
||||
caller never needed, so a missing row folds nothing rather than
|
||||
raising — which is also exactly how the feed behaved before this.
|
||||
"""
|
||||
settings = await self.session.get(ImportSettings, 1)
|
||||
return float(settings.discord_link_fold_hours) if settings is not None else 0.0
|
||||
|
||||
def _to_dict(
|
||||
self, post: Post, artist: Artist, source: Source | None,
|
||||
thumbs_map: dict, atts_map: dict, links_map: dict | None = None,
|
||||
unified_map: dict | None = None,
|
||||
) -> dict:
|
||||
plain_full = html_to_plain(post.description) if post.description else None
|
||||
if plain_full is None:
|
||||
@@ -436,6 +469,11 @@ class PostFeedService:
|
||||
# post is the drop). Always a list so the UI never branches on
|
||||
# absence.
|
||||
"associations": (links_map or {}).get(post.id, []),
|
||||
# #4402. On a teaser with a linked drop: what the card shows BY
|
||||
# REFERENCE — the drop's images, the piece's older variants, and
|
||||
# the text of each — plus who made each link, so one FC made by
|
||||
# itself can say so and offer the undo. None on every other post.
|
||||
"unified": (unified_map or {}).get(post.id),
|
||||
# Non-null on a chat message a synthetic post absorbed. The feed
|
||||
# filters these out, but `around`/`get_post` still reach them, and
|
||||
# the UI uses this to explain why a post it linked to is not in the
|
||||
|
||||
Reference in New Issue
Block a user