feat(playlist_track): server-side materialisation + share preview (phase 1.j.a) - #32
Merged
Merged
Conversation
…ase 1.j.a)
Closes the loop on Phase 1.g playlists: the apply pipeline now
writes `playlist + field: "tracks"` ops into a dedicated
`playlist_track` table, and the public share preview lists the
tracks instead of always returning `tracks: []`.
Schema mirrors the desktop SQLite shape at
`profile/20260411120000_initial.sql:236` — `(playlist_id, track_id)`
PK + position index, BIGINT epoch-millis for `added_at`. The
`track_id` column is the source desktop's local-i64 id (no FK
because the server has no `track` table yet — Phase 1.k territory).
Per-row snapshot columns (`snapshot_title`, `snapshot_artist`,
`snapshot_duration_ms`) carry the displayable values cross-device
so the public share preview can render the tracks without a
server-side track resolver.
Wire shape (additive — doesn't break the current desktop emitter):
- `payload.track_ids: [N, …]` (required) for insert + delete
- `payload.snapshots: { "<id_str>": { title, artist?, duration_ms? } }`
(optional, populated by the 1.j.b wire bump that desktops gain in
a follow-up release)
- `set tracks` carries `{ track_id, position }` for single-row
reorder
Snapshot fields land into `playlist_track` with `INSERT … ON
CONFLICT DO UPDATE SET … = COALESCE(EXCLUDED.…, playlist_track.…)`
so a future re-emit with richer metadata enriches the existing row
instead of clobbering it. Pre-1.j.b desktops emit ops without
`snapshots` — rows land with NULL snapshot fields and stay
invisible in the public share preview (`fetch_for_share` filters
`snapshot_title IS NOT NULL`); they become visible automatically
once any snapshot-aware client re-syncs the same playlist.
Parent-playlist lookup misses surface as `Skipped` (not `Applied`)
so the durable log keeps the op for replay once the playlist
insert lands.
Tests (6 new):
- `playlist_insert_tracks_materialises_rows_without_snapshot`
- `playlist_insert_tracks_carries_snapshot`
- `playlist_delete_tracks_drops_rows`
- `playlist_set_tracks_reorders_position`
- `playlist_insert_tracks_without_parent_is_skipped`
- `public_get_lists_tracks_when_snapshots_present`
- `public_get_hides_tracks_without_snapshots`
Follow-up tracked:
- 1.j.b — desktop wire bump (snapshot in tracks ops payload).
- 1.j.c — web `/p/$token` route renders the tracks from the
populated `tracks[]` array.
Signed-off-by: InstaZDLL <github.105mh@8shield.net>
📝 WalkthroughWalkthroughCette PR matérialise complètement les pistes de playlist côté serveur. Elle ajoute une table de jointure, implémente les opérations CRUD, intègre le traitement dans le pipeline d'apply, expose les pistes sur l'API publique via snapshot, et couvre l'ensemble avec des tests end-to-end. ChangesPlaylist Tracks End-to-End
Sequence Diagram(s)sequenceDiagram
participant Client
participant ApplyPipeline as Apply Pipeline
participant Lookup as lookup_playlist_id
participant DB as db::playlist_track
Client->>ApplyPipeline: sync op (insert/delete/set tracks)
ApplyPipeline->>ApplyPipeline: dispatch par field & op_type
alt insert_tracks
ApplyPipeline->>Lookup: resolve playlist_id
Lookup-->>ApplyPipeline: playlist_id | None
ApplyPipeline->>ApplyPipeline: track_ids_from_payload
ApplyPipeline->>ApplyPipeline: snapshots_from_payload (opt)
ApplyPipeline->>DB: append_tracks
else delete_tracks
ApplyPipeline->>Lookup: resolve playlist_id
ApplyPipeline->>ApplyPipeline: track_ids_from_payload
ApplyPipeline->>DB: remove_tracks
else set_tracks (reorder)
ApplyPipeline->>Lookup: resolve playlist_id
ApplyPipeline->>ApplyPipeline: validate {track_id, position}
ApplyPipeline->>DB: set_position
end
DB-->>ApplyPipeline: success | error
ApplyPipeline-->>Client: Applied | Skipped | Error
sequenceDiagram
participant Client
participant ShareHandler as share.rs handler
participant DB as db::playlist_track
Client->>ShareHandler: GET /api/v1/share/playlists/{token}
ShareHandler->>ShareHandler: lookup playlist par token
alt playlist trouvée
ShareHandler->>DB: fetch_for_share(playlist_id)
DB->>DB: SELECT * ORDER BY position<br/>WHERE snapshot_title IS NOT NULL
DB-->>ShareHandler: Vec<PublicTrackRow>
alt requête réussie
ShareHandler->>ShareHandler: map vers Vec<PublicTrack>
else erreur DB
ShareHandler->>ShareHandler: warn log, tracks = []
end
else playlist non trouvée
ShareHandler-->>Client: 404
end
ShareHandler-->>Client: PublicPlaylistResponse {tracks, ...}
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 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 |
This was referenced Jun 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the loop on Phase 1.g playlists. Until now
/api/v1/share/playlists/{token}always returnedtracks: []regardless of the playlist's actual content because the server had no entity table to materialise the desktop'splaylist + field: \"tracks\"ops into. This PR adds the table + the apply pipeline branches + the share preview SELECT.Wire change is additive — the current desktop emitter (
payload.track_ids: [N, …]only) keeps working. New optionalpayload.snapshotsmap carries the displayable title/artist/duration so the public share preview can render the rows without a server-side track resolver. Snapshot population lands in 1.j.b (desktop wire bump, future PR).Architecture decision
Per the post-1.g sprint plan, the playlist_track materialisation needed an architecture choice (file_hash refs / canonical id end-to-end / opaque server storage). User picked snapshot DTO côté desktop: minimal wire change (additive), server stocke le snapshot, share preview reste self-contained sans avoir besoin d'un track table server-side.
Wire shape
inserttracks\"tracks\"{ \"track_ids\": [N, …], \"snapshots\"?: { \"<id_str>\": { title, artist?, duration_ms? } } }deletetracks\"tracks\"{ \"track_ids\": [N, …] }settracks\"tracks\"{ \"track_id\": N, \"position\": M }(single-row reorder)Schema
playlist_trackmirrors the SQLite shape atprofile/20260411120000_initial.sql:236for parity withwaveflow-coretraits:playlist_id BIGINT FK ON DELETE CASCADEtrack_id BIGINT NOT NULL(no FK — server has notracktable yet, Phase 1.k territory)position INTEGER NOT NULL CHECK (position >= 0)added_at BIGINT NOT NULL(epoch-millis per CLAUDE.md convention)snapshot_title TEXT,snapshot_artist TEXT,snapshot_duration_ms BIGINT— all NULLable, populated by 1.j.b-and-later desktops(playlist_id, track_id)(playlist_id, position)for ordered scansBackward-compat strategy
payload.track_idsonly. Rows land with NULL snapshot fields.db::playlist_track::fetch_for_sharefiltersWHERE snapshot_title IS NOT NULL, so NULL-snapshot rows are invisible to the public preview.INSERT … ON CONFLICT DO UPDATE SET … = COALESCE(EXCLUDED.…, playlist_track.…)— a future re-emit with richer metadata enriches the existing row instead of clobbering it. Rows that started life NULL-snapshot become visible automatically once any snapshot-aware client re-syncs.Parent-playlist ordering
If a tracks op lands before its playlist's own
insert(out-of-order pull, retry race),lookup_playlist_idreturnsNoneand the apply handler returnsSkipped. The durable log keeps the op for replay; the next pass after the playlist materialises picks it up.Test plan
Follow-up
snapshotmap intracksops payload). Wire-additive, doesn't break older servers.Part of Sprint 2 of the post-1.g sprint plan.
Summary by CodeRabbit
Release Notes
New Features
Documentation