Skip to content

feat(feed): suppress recently-played tracks in for-you feed#950

Merged
dylanjeffers merged 1 commit into
mainfrom
feed-for-you-repeat-suppression
Jun 12, 2026
Merged

feat(feed): suppress recently-played tracks in for-you feed#950
dylanjeffers merged 1 commit into
mainfrom
feed-for-you-repeat-suppression

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

What

Adds repeat suppression to the For You feed: tracks the user has already played recently are scored lower so fresh content surfaces first.

How

  • New my_recent_plays CTE pulls the user's play history from the plays table, bounded to the most recent 2000 rows (same pattern as the affinity sub-select, per the performance notes from perf(for-you): cap my_saved_artists to 200 most-recent #805/perf(for-you): bound my_artist_affinity and follow_set by recency #806) before applying a 14-day window, keeping MAX(created_at) per track.
  • New repeat_penalty multiplier in the scoring CTE:
    • played in the last 7 days → 0.25x
    • played in the last 8–14 days → 0.5x
    • unplayed (or playlist) → 1.0x
  • repeat_penalty multiplies final_score alongside the existing social_boost, source_weight, and content_type_weight.
  • Suppressed tracks are not removed from the feed — they're just ranked lower.
  • Playlists always get 1.0x since there's no per-user playlist play data.

Tests

  • TestV1FeedForYou_RepeatSuppressionDownranksPlayedTracks — a 2-day-old play pushes a track below an otherwise lower-scored sibling, while keeping it in the feed.
  • TestV1FeedForYou_OldPlaysDoNotSuppress — a 20-day-old play is outside the window and does not downrank.

🤖 Generated with Claude Code

Tracks the user played in the last 14 days now get a repeat_penalty
multiplier on final_score (0.25x if played in the last 7 days, 0.5x in
the last 8-14 days, 1.0x otherwise) so the feed surfaces fresh content.
Suppressed tracks stay in the feed, just ranked lower. Playlists are
unaffected (no per-user playlist play data).

The play-history scan is bounded the same way as the affinity
sub-select (most recent 2000 rows before the 14-day window) so heavy
listeners can't pull the CTE wide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dylanjeffers dylanjeffers merged commit 2ff7cf1 into main Jun 12, 2026
5 checks passed
@dylanjeffers dylanjeffers deleted the feed-for-you-repeat-suppression branch June 12, 2026 04:13
dylanjeffers added a commit that referenced this pull request Jun 12, 2026
Item 3 of the for-you feed improvement queue (follows #949 content-type
weighting and #950 repeat suppression).

## What
Adds a **genre_affinity** multiplier to final_score, built from the
genre mix of the viewer's recent listening:

- New \`my_genre_affinity\` CTE: joins the viewer's most recent plays
(bounded to the last 1000, same pattern as the other play sub-selects)
to \`tracks.genre\` and computes each genre's share of plays.
- Multiplier: \`0.85 + 0.45 * min(genre_share / 0.30, 1)\` — a genre at
>=30% of recent listening gets the full **1.30x**, genres the viewer
never plays get **0.85x**.
- Neutral 1.00x for playlists (no single genre), genre-less tracks, and
cold-start users with no play history (no penalty when we know nothing).

## Tests
- \`TestV1FeedForYou_GenreAffinityBoostsFavoredGenre\`: an
all-Electronic listener sees the Electronic sibling outrank the
naturally-stronger Rock sibling, while the Rock track stays in the feed.
- \`TestV1FeedForYou_GenreAffinityNeutralOnColdStart\`: no play history
→ natural ranking unchanged.

\`go build ./...\` and the full \`go test ./...\` suite pass locally.

🤖 Opened by the automated feed-algorithm-improvement-loop.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant