Skip to content

feat(playlists): add on repeat smart playlist family#148

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/on-repeat-smart-playlist
May 25, 2026
Merged

feat(playlists): add on repeat smart playlist family#148
InstaZDLL merged 2 commits into
mainfrom
feat/on-repeat-smart-playlist

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 25, 2026

Summary

  • Adds On Repeat as a second built-in smart-playlist family next to Daily Mix. Materialises a single 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 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.
  • New regenerate_all_smart_playlists Tauri 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.
  • Custom branded cover artwork instead of an album-art composite: 640×640 JPEG with a deep indigo → royal violet diagonal gradient and a pink infinity-loop motif, rendered deterministically by cover::build_on_repeat_cover so 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_rules is 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_event source as Daily Mix, no recommendation graph or external metadata required, single playlist (no tempo bucketing).

Architectural notes

  • SmartPlaylistRules::OnRepeat is a unit variant — {"kind":"on_repeat"} is the upsert-by-needle key. Guarded by a cargo test so a serde rename can't silently break refresh-in-place.
  • generator::upsert_smart_playlist was refactored to take a needle: &str + position: i64 instead of a slot number so On Repeat (no slot) and Daily Mix can share the same upsert logic.
  • generator::first_track_artwork_paths is now pub(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 = 8 guard: 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

  • Regenerate via "Régénérer" on Home; verify On Repeat tile appears first in "Pour vous" carousel with the pink infinity-loop cover.
  • Switch profile; regenerate; verify On Repeat shows the new profile's top-played tracks (no cross-profile bleed).
  • Wipe play_event to <8 distinct tracks; regenerate; verify the On Repeat row is deleted (no stale playlist).
  • Same regen against unchanged listening data → same cover_hash (cache dedupe).
  • Locale switch: French / English / Arabic — eyebrow uses translated "On Repeat" label, layout untouched.

Summary by CodeRabbit

  • Nouvelles Fonctionnalités

    • Playlist "On Repeat" : affiche vos ~30 titres les plus joués sur 30 jours, matérialisée uniquement si suffisamment de titres.
    • Génération flexible : régénération globale (Daily Mix + On Repeat) en une seule commande, et régénération individuelle d’On Repeat.
    • UI améliorée : carousel "Made for you" distingue familles et utilise des visuels de secours adaptés.
  • Documentation

    • Docs mises à jour décrivant l’algorithme On Repeat et le flux de régénération.
  • Internationalisation

    • "On Repeat" ajouté dans 15+ langues.

Review Change Stack

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.
@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) scope: i18n Translations (src/i18n/) scope: docs Docs, README, assets type: feat New feature size: xl > 500 lines labels May 25, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

Ce 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.

Changes

On Repeat Smart Playlist Family

Layer / File(s) Summary
Schema et contrats de données
src-tauri/src/commands/playlist.rs, src-tauri/src/smart_playlists/mod.rs
Ajout du champ smart_rules: Option<String> au type public Playlist et ajout de SmartPlaylistRules::OnRepeat.
On Repeat regeneration logic
src-tauri/src/smart_playlists/on_repeat.rs
Fenêtre 30 jours, requête SQL top_played_tracks, seuil minimal (≥8 titres), suppression si insuffisant, construction d'IDs et upsert via upsert_smart_playlist, retourne Option.
Deterministic On Repeat cover generation
src-tauri/src/smart_playlists/cover.rs
build_on_repeat_cover() : rendu canvas 640×640 (gradient indigo→violet + motif infinity-loop), encodage JPEG, hash blake3, écriture dédupliquée dans metadata_artwork/, helper testable et test pixel.
Smart playlist persistence refactoring
src-tauri/src/smart_playlists/generator.rs
upsert_smart_playlist accepte maintenant needle (clé LIKE) et position; generate_one_mix construit needle et passe position; first_track_artwork_paths devient pub(super).
Tauri command exposure
src-tauri/src/commands/smart_playlists.rs, src-tauri/src/lib.rs
Ajout des commandes Tauri regenerate_on_repeat() et regenerate_all_smart_playlists() et enregistrement des handlers.
Frontend type bindings and discrimination
src/lib/tauri/playlist.ts
Ajout de smart_rules: string | null à Playlist, type SmartPlaylistKind et fonction smartPlaylistKind() pour classifier daily_mix/on_repeat.
Frontend smart playlists API
src/lib/tauri/smart_playlists.ts
Ajout des fonctions regenerateOnRepeat() et regenerateAllSmartPlaylists() et interface RegenerateAllSmartPlaylistsOutput.
HomeView UI integration
src/components/views/HomeView.tsx
Remplacement de l'appel de regen par regenerateAllSmartPlaylists; rendu du carrousel "Made for you" adapté selon le kind (label, fallback gradient, focus ring).
Localization strings
src/i18n/locales/*
Ajout de home.dailyMix.onRepeat.label ("On Repeat") dans les locales modifiées.
Documentation and CLAUDE.md updates
CLAUDE.md, docs/features/smart-playlists.md
Documentation de l'algorithme On Repeat (30j, tri par play count, pas de shuffle, seuil de diversité) et mention de regenerate_all_smart_playlists.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#39: étend l’export/restore de metadata_artwork/**, lié à la cache utilisée par la nouvelle jaquette "On Repeat".

Poem

🎶 Trente jours qui murmurent en boucle,
Un dégradé indigo au violet qui s'enroule,
Anneaux roses tissent une marque sans mot,
La playlist se reconstruit, fidèle écho,
On Repeat revient, immuable et beau.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre décrit précisément l'ajout du système de playlist « On Repeat » comme famille de playlists intelligentes, ce qui correspond au changement principal du PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed La description suit le template requis avec un résumé détaillé, une justification architecturale, et une liste de tests complète.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/on-repeat-smart-playlist

Comment @coderabbitai help to get the list of available commands and usage tips.

@InstaZDLL InstaZDLL self-assigned this May 25, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Mettre à jour position aussi dans la branche UPDATE.

upsert_smart_playlist reçoit désormais position, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 85c877b and 1219e6b.

📒 Files selected for processing (29)
  • CLAUDE.md
  • docs/features/smart-playlists.md
  • src-tauri/src/commands/playlist.rs
  • src-tauri/src/commands/smart_playlists.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/smart_playlists/cover.rs
  • src-tauri/src/smart_playlists/generator.rs
  • src-tauri/src/smart_playlists/mod.rs
  • src-tauri/src/smart_playlists/on_repeat.rs
  • src/components/views/HomeView.tsx
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/id.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/nl.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/zh-CN.json
  • src/i18n/locales/zh-TW.json
  • src/lib/tauri/playlist.ts
  • src/lib/tauri/smart_playlists.ts

Comment thread docs/features/smart-playlists.md Outdated
Comment thread src-tauri/src/commands/smart_playlists.rs
Comment thread src/components/views/HomeView.tsx
Comment thread src/i18n/locales/zh-CN.json
Comment thread src/i18n/locales/zh-TW.json
… 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.
@InstaZDLL
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@InstaZDLL InstaZDLL merged commit a9400ca into main May 25, 2026
14 checks passed
@InstaZDLL InstaZDLL deleted the feat/on-repeat-smart-playlist branch May 25, 2026 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: xl > 500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant