feat(playlists): add on repeat smart playlist family#148
Conversation
Materialises a single "On Repeat" playlist from the active profile's last 30 days of play_event rows — top ~30 tracks by play count, ordered descending so the user's current rotation lands at the top of the "Pour vous" carousel. Single round-trip "Régénérer" via regenerate_all_smart_playlists refreshes Daily Mix slots + On Repeat together so the UI doesn't need to know about the family split. Cover is a deterministic brand artwork (indigo→violet diagonal gradient + pink infinity-loop motif) rather than an album-art composite, matching Spotify's fixed visual identity for On Repeat. No text is rasterised so the canvas stays locale-agnostic; the playlist name and family eyebrow render in React on top of the tile. The playlist row exposes smart_rules so the frontend can switch styling per family without name-pattern fragility.
📝 WalkthroughWalkthroughCe PR ajoute la playlist smart "On Repeat" (top ~30 titres des 30 derniers jours), la génération de cover déterministe, un upsert smart playlist généralisé, deux commandes Tauri (incl. regen tout-en-un), et l’intégration frontend + i18n. ChangesOn Repeat Smart Playlist Family
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src-tauri/src/smart_playlists/generator.rs (1)
524-530:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMettre à jour
positionaussi dans la branche UPDATE.
upsert_smart_playlistreçoit désormaisposition, mais un playlist smart existant ne voit jamais cette valeur appliquée. Résultat: ordre potentiellement périmé malgré la régénération.✅ Correctif proposé
UPDATE playlist SET name = ?, description = ?, cover_hash = ?, smart_rules = ?, + position = ?, updated_at = ? WHERE id = ?.bind(name) .bind(description) .bind(cover_hash) .bind(rules_json) + .bind(position) .bind(now) .bind(id)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src-tauri/src/smart_playlists/generator.rs` around lines 524 - 530, La branche UPDATE de upsert_smart_playlist n'inclut pas la colonne position, donc quand une playlist smart existante est mise à jour sa position n'est jamais appliquée; modify the UPDATE SQL in upsert_smart_playlist to set position = ? alongside name, description, cover_hash, smart_rules, updated_at and ensure the corresponding parameter for position is added to the prepared statement/parameter list in the same order as the placeholders so existing playlists receive the new position value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/features/smart-playlists.md`:
- Around line 73-86: The doc text and SQL disagree: the prose says "the top ~30
tracks" while the query uses "LIMIT 60" in on_repeat.rs; update the
documentation to be explicit and consistent by either (A) changing the prose to
"up to 60 tracks" to match the SQL, or (B) documenting the extra truncation step
(mentioning that the query returns up to 60 rows and the code in on_repeat.rs
then truncates/filters that set down to ~30 tracks) and ensuring the exact
truncation logic and resulting count in on_repeat.rs is described (reference the
SQL LIMIT 60 and the file on_repeat.rs to find the truncation logic).
In `@src-tauri/src/commands/smart_playlists.rs`:
- Around line 47-58: regenerate_all_smart_playlists is not atomic:
generator::regenerate_daily_mixes is persisted before
on_repeat::regenerate_on_repeat, so failures leave a partial state; make the two
operations run inside a single database transaction (or equivalent atomic unit)
and only commit after both succeed — e.g., obtain a transactional connection
from the pool (pool.begin_transaction() / pool.transaction(...) depending on
your DB crate), pass that transaction into generator::regenerate_daily_mixes and
on_repeat::regenerate_on_repeat (or refactor those functions to accept a &mut
Transaction), perform both calls, and commit the transaction; if either call
errors, roll back and return the error so the UI never sees a partial update.
In `@src/components/views/HomeView.tsx`:
- Around line 470-472: The i18n key used for the "On Repeat" label is incorrect:
change the t() call in HomeView where eyebrow is set (the ternary using
isOnRepeat and t(...)) to use "home.dailyMix.onRepeat.label" instead of
"home.onRepeat.label", and then add that new key to all locale JSONs under
src/i18n/locales/* so every of the 17 locales contains
"home.dailyMix.onRepeat.label" (with appropriate translations) to avoid falling
back to the hardcoded "On Repeat".
In `@src/i18n/locales/zh-CN.json`:
- Around line 296-298: The i18n key was added at home.onRepeat.label but the
feature expects home.dailyMix.onRepeat.label; update the JSON so the key is
nested under "home" → "dailyMix" → "onRepeat" → "label" (i.e., rename/move
home.onRepeat.label to home.dailyMix.onRepeat.label) and ensure the same key
exists in all other locale files to satisfy the contract.
In `@src/i18n/locales/zh-TW.json`:
- Around line 296-298: The i18n key was added under the wrong namespace: move or
rename the key "home.onRepeat.label" to "home.dailyMix.onRepeat.label" so the
zh-TW locale matches the Daily Mix key family used by other locales; update the
JSON entry accordingly and verify no duplicate "onRepeat" exists at top-level
"home" to keep the schema consistent.
---
Outside diff comments:
In `@src-tauri/src/smart_playlists/generator.rs`:
- Around line 524-530: La branche UPDATE de upsert_smart_playlist n'inclut pas
la colonne position, donc quand une playlist smart existante est mise à jour sa
position n'est jamais appliquée; modify the UPDATE SQL in upsert_smart_playlist
to set position = ? alongside name, description, cover_hash, smart_rules,
updated_at and ensure the corresponding parameter for position is added to the
prepared statement/parameter list in the same order as the placeholders so
existing playlists receive the new position value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f55bcdfe-d478-4ff6-b203-47a8c181ad54
📒 Files selected for processing (29)
CLAUDE.mddocs/features/smart-playlists.mdsrc-tauri/src/commands/playlist.rssrc-tauri/src/commands/smart_playlists.rssrc-tauri/src/lib.rssrc-tauri/src/smart_playlists/cover.rssrc-tauri/src/smart_playlists/generator.rssrc-tauri/src/smart_playlists/mod.rssrc-tauri/src/smart_playlists/on_repeat.rssrc/components/views/HomeView.tsxsrc/i18n/locales/ar.jsonsrc/i18n/locales/de.jsonsrc/i18n/locales/en.jsonsrc/i18n/locales/es.jsonsrc/i18n/locales/fr.jsonsrc/i18n/locales/hi.jsonsrc/i18n/locales/id.jsonsrc/i18n/locales/it.jsonsrc/i18n/locales/ja.jsonsrc/i18n/locales/ko.jsonsrc/i18n/locales/nl.jsonsrc/i18n/locales/pt-BR.jsonsrc/i18n/locales/pt.jsonsrc/i18n/locales/ru.jsonsrc/i18n/locales/tr.jsonsrc/i18n/locales/zh-CN.jsonsrc/i18n/locales/zh-TW.jsonsrc/lib/tauri/playlist.tssrc/lib/tauri/smart_playlists.ts
… repeat docs UPDATE branch of upsert_smart_playlist was missing position, so an existing smart playlist row would silently keep its first-regen sort order even if the family's PLAYLIST_POSITION constant changed between releases (e.g. shifting On Repeat from 1 to 0 to land ahead of Daily Mix in the carousel). Set position alongside the other refresh columns so re-anchoring actually takes effect. Also clarify docs/features/smart-playlists.md: the SQL query has LIMIT 60 as a candidate-pool buffer and the Rust caller truncates to TRACKS_LIMIT = 30 before the upsert. The previous prose said "top ~30 tracks" without explaining the over-fetch, which made the embedded SQL look inconsistent with the constant.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Summary
play_eventrows: top ~30 tracks by play count, ordered descending so the user's current build(deps): bump rustls-webpki from 0.103.11 to 0.103.13 in /src-tauri in the cargo group across 1 directory #1 rotation lands at the top of the "Pour vous" carousel.regenerate_all_smart_playlistsTauri command refreshes every built-in family (Daily Mix slots + On Repeat) in a single round-trip — Home's "Régénérer" button now refreshes the whole "Made for you" surface atomically without the frontend having to know about the split.cover::build_on_repeat_coverso identical regens dedupe against the cache instead of piling up orphan files. No text is rasterised — the playlist name + family eyebrow render in React on top of the tile so the canvas stays locale-agnostic.Playlist.smart_rulesis now exposed to the frontend so the carousel can switch styling per family (different eyebrow, gradient fallback colour, focus-ring tint) via a parsed discriminant rather than fragile name-pattern matching.Why On Repeat first
Of the candidate auto-playlists (On Repeat, Repeat Rewind, Release Radar, Time Capsule), On Repeat is the most useful day-to-day surface and the simplest to ship: same
play_eventsource as Daily Mix, no recommendation graph or external metadata required, single playlist (no tempo bucketing).Architectural notes
SmartPlaylistRules::OnRepeatis a unit variant —{"kind":"on_repeat"}is the upsert-by-needle key. Guarded by acargo testso a serde rename can't silently break refresh-in-place.generator::upsert_smart_playlistwas refactored to take aneedle: &str+position: i64instead of a slot number so On Repeat (no slot) and Daily Mix can share the same upsert logic.generator::first_track_artwork_pathsis nowpub(super)so future per-track-art families (Release Radar, Recently Added) can resolve the first track's artist image without duplicating the query.MIN_TRACKS = 8guard: below that the playlist would be "the same handful of songs you already listened to a lot" and adds nothing over the History view. A previously-materialised row is deleted when the guard kicks in so a stale playlist doesn't linger after a quiet month.Test plan
play_eventto <8 distinct tracks; regenerate; verify the On Repeat row is deleted (no stale playlist).cover_hash(cache dedupe).Summary by CodeRabbit
Nouvelles Fonctionnalités
Documentation
Internationalisation