Skip to content

feat(sync): emit per-track snapshots in playlist tracks ops (phase 1.j.b)#203

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/1-j-b-desktop-playlist-track-snapshots
Jun 6, 2026
Merged

feat(sync): emit per-track snapshots in playlist tracks ops (phase 1.j.b)#203
InstaZDLL merged 2 commits into
mainfrom
feat/1-j-b-desktop-playlist-track-snapshots

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented Jun 6, 2026

Summary

Outbound wire bump that follows server-side PR #32 (closes the share-preview loop for shared playlists). Every command that emits a `playlist + field: "tracks", op: "insert"` op now folds a per-track `snapshots` map into the payload alongside `track_ids`, so the server's `playlist_track.snapshot_*` columns land populated and the public share preview at `/api/v1/share/playlists/{token}` can render the title + artist + duration without resolving the local-i64 track id cross-device.

Wire shape

{
  \"track_ids\": [42, 43],
  \"snapshots\": {
    \"42\": { \"title\": \"One More Time\", \"artist\": \"Daft Punk\", \"duration_ms\": 320000 },
    \"43\": { \"title\": \"Around the World\", \"duration_ms\": 280000 }
  }
}
  • `artist` is omitted (not `null`) when no `track_artist` row exists for the track.
  • `duration_ms` is always present.
  • A track id whose row was deleted between the playlist write and the snapshot SELECT is silently dropped from the map — the server tolerates the id-without-snapshot case (row lands NULL-snapshot, invisible to the public preview until the next re-emit).

Backward compat

The change is additive — older servers that don't know the `snapshots` field ignore it and store just the `track_ids`. waveflow-server is on the new shape since PR #32 mergé.

Sites bumped (all inside the existing SQLite tx)

  • `add_track_to_playlist` (single track)
  • `add_tracks_to_playlist` (bulk insert)
  • `add_tracks_from_source` (folder / album / artist bulk)

`remove_track_from_playlist` and `reorder_playlist_track` keep their minimal payloads (no display change on the receiving side).

New module

`sync::track_snapshots` runs the GROUP_CONCAT artist query (mirrors `repository/sqlite/track.rs:33`) and returns the map. Uses `sqlx::AssertSqlSafe` for the `IN (?, ?, …)` expansion — same audited pattern as `commands/radio.rs:225`. `iter::repeat().take(n)` rather than the newer `repeat_n` helper because the repo's MSRV is 1.80 (`repeat_n` is stable since 1.82).

Test plan

  • `cargo fmt --all --check` ✅ locally
  • `cargo clippy --all-targets --message-format=short -- -D warnings` ✅ locally
  • `cargo test -p waveflow --lib track_snapshots` — 5 new unit tests pass (empty input, single track + artist, multi-artist join string, missing track id dropped, artist-absent omits the field)
  • `cargo test -p waveflow --lib` — 128/128 lib tests pass (existing suite unchanged)

Follow-up

Part of Sprint 2 of the post-1.g sprint plan validated 2026-06-05.

Includes scattered `cargo fmt` cleanups on `commands/share.rs`, `db/profile_meta.rs`, `sync/drain.rs` — pure whitespace, picked up by the formatter when I ran it on the snapshot module.

Summary by CodeRabbit

Notes de version

  • Nouvelles fonctionnalités

    • Les snapshots de tracks sont désormais inclus automatiquement lors de l'ajout de pistes aux playlists, permettant un meilleur affichage des aperçus partagés.
  • Refactorisation

    • Amélioration générale de la mise en forme et de la structure du code pour une meilleure maintenabilité.

…j.b)

Outbound wire bump that follows server PR #32. Every command that
emits a `playlist + field: "tracks", op: "insert"` op now folds a
per-track `snapshots` map into the payload alongside `track_ids`,
so the server's `playlist_track.snapshot_*` columns land populated
and the public share preview at `/api/v1/share/playlists/{token}`
can render the title + artist + duration without resolving the
local-i64 track id cross-device.

Shape:
{
  "track_ids": [42, 43],
  "snapshots": {
    "42": { "title": "One More Time", "artist": "Daft Punk",  "duration_ms": 320000 },
    "43": { "title": "Around the World",                       "duration_ms": 280000 }
  }
}

`artist` is omitted (not null) when no `track_artist` row exists.
`duration_ms` is always present. A track id whose row was deleted
between the playlist write and the snapshot SELECT is silently
dropped from the map; the server tolerates the id-without-snapshot
case (row lands NULL-snapshot, invisible to the public preview).

Three emit sites bumped — all inside the same SQLite transaction
as the playlist write + the outbox enqueue:
- `add_track_to_playlist`
- `add_tracks_to_playlist`
- `add_tracks_from_source`

`remove_track_from_playlist` and `reorder_playlist_track` keep their
minimal payloads (no display change on the receiving side).

New module `sync::track_snapshots` runs the GROUP_CONCAT artist
query (mirrors the SELECT in `repository/sqlite/track.rs:33`) and
returns the map. Uses `sqlx::AssertSqlSafe` for the `IN (?, ?, …)`
expansion, same audited pattern as `commands/radio.rs:225`.
`iter::repeat().take(n)` rather than `repeat_n` to respect the
1.80 MSRV.

Tests: 5 unit tests on the snapshot builder (empty input, single
track + artist, multi-artist join string, missing track id dropped,
artist-absent omits the field). 128/128 lib tests pass.

Includes scattered `cargo fmt` cleanups on `commands/share.rs`,
`db/profile_meta.rs`, `sync/drain.rs` — pure whitespace, picked up
by the formatter when I ran it on the snapshot module.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets type: feat New feature size: l 200-500 lines labels Jun 6, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 6, 2026

Review Change Stack

Warning

Review limit reached

@InstaZDLL, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 34 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2f222957-7b7a-4255-b68d-e18cdfb458ef

📥 Commits

Reviewing files that changed from the base of the PR and between fab8d77 and f45d372.

📒 Files selected for processing (1)
  • CLAUDE.md
📝 Walkthrough

Walkthrough

Ce PR ajoute une couche de métadonnées snapshot pour les pistes insérées dans des playlists. Un nouveau module track_snapshots construit du JSON (titre, durée, artistes) depuis la base SQLite et l'intègre au payload sync, permettant au serveur d'afficher des prévisualisations de partage sans resync.

Changes

Track snapshots for playlist operations

Layer / File(s) Summary
Track snapshot builder module
src-tauri/crates/app/src/sync/track_snapshots.rs
Fonction build_snapshots asynchrone qui génère un objet JSON indexé par id de track contenant title, duration_ms, et artist (via GROUP_CONCAT ordonné par position). Retourne {} si vide ; omit les ids absents en base. Suite de tests complète couvrant entrée vide, peuplement multi-artistes, ids manquants, et omission du champ artist quand aucun artiste n'existe.
Sync module wiring
src-tauri/crates/app/src/sync/mod.rs
Export du nouveau sous-module track_snapshots pour le rendre accessible aux commandes de playlist.
Playlist command integration
src-tauri/crates/app/src/commands/playlist.rs
add_track_to_playlist, add_tracks_to_playlist, et add_source_to_playlist appellent maintenant build_snapshots et attachent le JSON résultant au payload de l'opération insert sur tracks dans l'outbox.
Snapshot rule documentation
CLAUDE.md
Nouvelle règle cross-cutting : les opérations d'insertion de pistes doivent générer snapshots dans la même transaction SQLite et les inclure sous la clé snapshots. Le serveur affiche les prévisualisations de partage uniquement pour les pistes avec snapshot_title IS NOT NULL. Opérations delete et set (reorder) exemptées.

Note: Plusieurs changements de formatage (share.rs, profile_meta.rs, drain.rs) purement cosmétiques sans impact logique sont omis de cette analyse.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#192: Modifie également playlist.rs sur les opérations insert/set du champ tracks pour enrichir le payload sync.
  • InstaZDLL/WaveFlow#195: Mutations de playlist côté playlist.rs autour de l'enqueue atomique des opérations sync via le même flux d'écriture.

Suggested labels

type: feat, size: l, scope: backend, scope: docs

Poem

📸 Des snapshots de piste, saisis dans le temps,
Titre, artiste, durée — les métadonnées dansent.
SQLite murmure, outbox transporte,
Partages scintillent sans resync, la magie qui emporte. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre décrit clairement le changement principal : ajout d'émission de snapshots de pistes dans les opérations de playlist côté sync, suivi d'une phase spécifique (1.j.b).
Description check ✅ Passed La description couvre les aspects essentiels (résumé du changement, forme du fil de fer, compatibilité rétroactive, tests), mais la section 'How I tested' du template n'est pas explicitement complétée en tant que telle.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1-j-b-desktop-playlist-track-snapshots

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

@InstaZDLL InstaZDLL self-assigned this Jun 6, 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: 1

🤖 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 `@CLAUDE.md`:
- Line 84: The documented command name is incorrect: the docs reference
add_tracks_from_source but the actual handler in commands/playlist.rs is
add_source_to_playlist; update CLAUDE.md to use the real handler name
(add_source_to_playlist) where the rule discusses calling
sync::track_snapshots::build_snapshots(conn, &track_ids) and folding snapshots
into the outbound payload (so references to playlist_track.snapshot_* and the
snapshot filtering behavior remain accurate), or alternatively rename the
handler to add_tracks_from_source if you prefer code-side consistency—ensure doc
and code use the exact same symbol (add_source_to_playlist or
add_tracks_from_source) so grep and future changes are reliable.
🪄 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: 8698e4ae-12a3-4e33-93e6-a4299eba3836

📥 Commits

Reviewing files that changed from the base of the PR and between 05be1e5 and fab8d77.

📒 Files selected for processing (7)
  • CLAUDE.md
  • src-tauri/crates/app/src/commands/playlist.rs
  • src-tauri/crates/app/src/commands/share.rs
  • src-tauri/crates/app/src/db/profile_meta.rs
  • src-tauri/crates/app/src/sync/drain.rs
  • src-tauri/crates/app/src/sync/mod.rs
  • src-tauri/crates/app/src/sync/track_snapshots.rs

Comment thread CLAUDE.md Outdated
CodeRabbit flagged the rule referenced `add_tracks_from_source`, but
the actual handler in `commands/playlist.rs:551` is `add_source_to_playlist`.
Fix the name so a grep against the doc lands on the real call site.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@InstaZDLL
Copy link
Copy Markdown
Owner Author

@coderabbitai valide, j'avais inversé. Le handler côté code est add_source_to_playlist (commands/playlist.rs:551), pas add_tracks_from_source. CLAUDE.md updated en f45d372.

Le commit message original mentionne aussi le mauvais nom mais je le laisse — le git log est immutable et c'est CLAUDE.md qui sert de doc vivante (grep target).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 6, 2026

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL : Noté — add_source_to_playlist est bien le nom correct. La mise à jour de CLAUDE.md en f45d372 est la bonne approche ; c'est effectivement la doc vivante qui prime pour les grep futurs.


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/203

Timestamp: 2026-06-06T20:54:58.044Z
Learning: In WaveFlow (InstaZDLL/WaveFlow), the playlist command handler that adds tracks from a source is named `add_source_to_playlist` (at `src-tauri/crates/app/src/commands/playlist.rs` line ~551), NOT `add_tracks_from_source`. The PR `#203` commit message used the wrong name but CLAUDE.md (updated in f45d372) has the correct name and serves as the canonical living documentation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: InstaZDLL
Repo: InstaZDLL/WaveFlow PR: 192
File: src-tauri/crates/app/src/commands/playlist.rs:105-123
Timestamp: 2026-05-31T16:39:07.329Z
Learning: In WaveFlow (InstaZDLL/WaveFlow), the outbox writes in `src-tauri/crates/app/src/commands/playlist.rs` are intentionally not atomic with local playlist mutations in PR `#192` (phase 1.f.desktop.2b). The target fix (wrapping both in the same SQLite transaction) is deferred to a future PR. The planned API shape is `enqueue_op_in_tx(tx, draft)` + `tx.commit()`. The drain task at phase 1.f.desktop.4 reconciles any lost ops via `operation_id`. The bounded window is 2 consecutive SQLite commits on the same pool.

Learnt from: CR
Repo: InstaZDLL/WaveFlow PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-05-30T00:14:40.564Z
Learning: Applies to src-tauri/crates/app/src/commands/**/*.rs : Commands that rewrite audio files (`edit::update_track_tags`, `save_lyrics`, `set_track_rating`) must pause playback first when the edited track is `current_track_id`, then re-hash with blake3 and update `track.file_hash` after the write

Learnt from: InstaZDLL
Repo: InstaZDLL/WaveFlow PR: 0
File: :0-0
Timestamp: 2026-05-24T10:42:21.505Z
Learning: Dans RFC-001 de WaveFlow (PR `#127`, commit d8a82e8), trois mécanismes ont été ajoutés au protocole de sync :
1. Table `sync_compaction_watermark (user_id, compacted_up_to, updated_at)` : le Pull handler renvoie 410 Gone (REST) ou frame WS `{"type":"resync_required","compacted_up_to":N}` si `since < compacted_up_to` ; le client wipe son état dérivé et re-pull depuis `since=0`.
2. Index composite `(user_id, last_seen_at, last_seen_id)` sur `device_sync_cursor` pour index-only scan sur `MIN(last_seen_id) WHERE user_id=? AND last_seen_at > NOW() - INTERVAL '90 days'`.
3. ACK debounce serveur : accumulation in-memory par `(user_id, device_id)`, flush toutes les 5s ou à la déconnexion. Invariant de sécurité : le MIN de compaction lit la valeur in-memory → un crash avant flush laisse Postgres conservateur (jamais de compaction prématurée). `sync_compaction_watermark` doit être mis à jour atomiquement avec la suppression des ops dans la même transaction.

Learnt from: InstaZDLL
Repo: InstaZDLL/WaveFlow PR: 148
File: src-tauri/src/commands/smart_playlists.rs:47-58
Timestamp: 2026-05-25T03:21:35.215Z
Learning: In `src-tauri/src/commands/smart_playlists.rs`, `regenerate_all_smart_playlists` is intentionally NOT wrapped in a single cross-family database transaction. Daily Mix and On Repeat are independent smart-playlist families; each family already guarantees intra-family atomicity via `pool.begin()` inside `upsert_smart_playlist`. Rolling back both families on a partial failure (e.g., On Repeat cover render failing due to a full disk) would discard perfectly valid Daily Mix data. Inter-family atomicity has no business meaning here — best-effort per family is the intended design.

@InstaZDLL InstaZDLL merged commit acd9215 into main Jun 6, 2026
14 checks passed
@InstaZDLL InstaZDLL deleted the feat/1-j-b-desktop-playlist-track-snapshots branch June 6, 2026 20:58
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 size: l 200-500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant