description: Plan and implement Stage 6 Playlists with CRUD endpoints, track management, and reordering (plan)
Implement Playlists Skill
Plan and implement Stage 6 Playlists for NovaTune: CRUD endpoints, track management, stable ordering, and lifecycle integration.
Overview
Stage 6 implements playlist management with:
- GET /playlists - List playlists with search and cursor-based pagination
- POST /playlists - Create playlist with quota enforcement
- GET /playlists/{playlistId} - Get playlist with paginated tracks
- PATCH /playlists/{playlistId} - Update playlist metadata
- DELETE /playlists/{playlistId} - Hard delete playlist
- POST /playlists/{playlistId}/tracks - Add tracks at position
- DELETE /playlists/{playlistId}/tracks/{position} - Remove track
- POST /playlists/{playlistId}/reorder - Reorder tracks
Implementation Plan
Phase 1: Models and Configuration
-
Create Playlist Model (
ApiService/Models/Playlist.cs)PlaylistId(ULID)UserId(owner)Name,DescriptionTracks(embeddedList<PlaylistTrackEntry>)TrackCount,TotalDuration(denormalized)Visibilityenum (Private, Unlisted, Public)CreatedAt,UpdatedAt
-
Create PlaylistTrackEntry (
ApiService/Models/PlaylistTrackEntry.cs)Position(0-based index)TrackId(ULID reference)AddedAt
-
Add Configuration (
ApiService/Configuration/PlaylistOptions.cs)MaxPlaylistsPerUser(default: 200)MaxTracksPerPlaylist(default: 10,000)MaxTracksPerAddRequest(default: 100)MaxMovesPerReorderRequest(default: 50)MaxNameLength(default: 100)MaxDescriptionLength(default: 500)DefaultPageSize(default: 20)MaxPageSize(default: 50)
-
Add DTOs (
ApiService/Models/)PlaylistListQuery,PlaylistDetailQueryPlaylistListItem,PlaylistDetails,PlaylistTrackItemCreatePlaylistRequest,UpdatePlaylistRequestAddTracksRequest,ReorderRequest,MoveOperation
Phase 2: RavenDB Indexes
-
Playlists_ByUserForSearch (
ApiService/Infrastructure/Indexes/)Map = playlists => from playlist in playlists select new { playlist.UserId, playlist.Name, playlist.TrackCount, playlist.CreatedAt, playlist.UpdatedAt, SearchText = playlist.Name }; Index("SearchText", FieldIndexing.Search); -
Playlists_ByTrackReference (
ApiService/Infrastructure/Indexes/)Map = playlists => from playlist in playlists from track in playlist.Tracks select new { UserId = playlist.UserId, PlaylistId = playlist.PlaylistId, TrackId = track.TrackId };
Phase 3: Service Layer
-
IPlaylistService (
ApiService/Services/)ListPlaylistsAsync(userId, query, ct)CreatePlaylistAsync(userId, request, ct)GetPlaylistAsync(playlistId, userId, query, ct)UpdatePlaylistAsync(playlistId, userId, request, ct)DeletePlaylistAsync(playlistId, userId, ct)AddTracksAsync(playlistId, userId, request, ct)RemoveTrackAsync(playlistId, userId, position, ct)ReorderTracksAsync(playlistId, userId, request, ct)RemoveDeletedTrackReferencesAsync(trackId, userId, ct)
-
Custom Exceptions (
ApiService/Infrastructure/Exceptions/)PlaylistNotFoundExceptionPlaylistAccessDeniedExceptionPlaylistQuotaExceededExceptionPlaylistTrackLimitExceededExceptionPlaylistTrackNotFoundExceptionInvalidPositionException
Phase 4: API Endpoints
-
PlaylistEndpoints.cs (
ApiService/Endpoints/)group.MapGet("/", HandleListPlaylists).RequireRateLimiting("playlist-list"); group.MapPost("/", HandleCreatePlaylist).RequireRateLimiting("playlist-create"); group.MapGet("/{playlistId}", HandleGetPlaylist); group.MapPatch("/{playlistId}", HandleUpdatePlaylist).RequireRateLimiting("playlist-update"); group.MapDelete("/{playlistId}", HandleDeletePlaylist).RequireRateLimiting("playlist-delete"); group.MapPost("/{playlistId}/tracks", HandleAddTracks).RequireRateLimiting("playlist-tracks-add"); group.MapDelete("/{playlistId}/tracks/{position:int}", HandleRemoveTrack).RequireRateLimiting("playlist-tracks-remove"); group.MapPost("/{playlistId}/reorder", HandleReorderTracks).RequireRateLimiting("playlist-reorder"); -
Rate Limiting Policies
playlist-list: 60 req/minplaylist-create: 20 req/minplaylist-update: 30 req/minplaylist-delete: 20 req/minplaylist-tracks-add: 30 req/minplaylist-tracks-remove: 60 req/minplaylist-reorder: 30 req/min
Phase 5: Track Validation
When adding tracks to playlists:
- Verify track IDs are valid ULIDs
- Verify tracks exist in RavenDB
- Verify tracks are owned by the same user
- Verify tracks are not deleted (
Status != Deleted) - Verify playlist track limit not exceeded
var trackDocs = await _session.LoadAsync<Track>(
request.TrackIds.Select(id => $"Tracks/{id}"), ct);
foreach (var (trackId, track) in trackDocs)
{
if (track is null)
throw new TrackNotFoundException(trackId);
if (track.UserId != userId)
throw new TrackAccessDeniedException(trackId);
if (track.Status == TrackStatus.Deleted)
throw new TrackDeletedException(trackId);
}
Phase 6: Position Management
Adding tracks:
var insertPosition = request.Position ?? playlist.Tracks.Count;
// Shift existing tracks
foreach (var entry in playlist.Tracks.Where(t => t.Position >= insertPosition))
entry.Position += request.TrackIds.Count;
// Add new tracks
var newEntries = request.TrackIds.Select((id, i) => new PlaylistTrackEntry
{
Position = insertPosition + i,
TrackId = id,
AddedAt = now
});
playlist.Tracks.AddRange(newEntries);
Removing tracks:
playlist.Tracks.Remove(trackToRemove);
// Reindex positions
foreach (var entry in playlist.Tracks.Where(t => t.Position > position))
entry.Position--;
Reordering tracks:
foreach (var move in request.Moves)
{
var track = tracks[move.From];
tracks.RemoveAt(move.From);
tracks.Insert(move.To, track);
}
// Reassign positions
for (var i = 0; i < tracks.Count; i++)
tracks[i].Position = i;
Phase 7: Lifecycle Integration
Extend lifecycle worker to clean up playlist references when tracks are physically deleted:
- Query
Playlists_ByTrackReferenceindex to find affected playlists - Remove all entries for the deleted track
- Reindex positions
- Update denormalized
TrackCountandTotalDuration
Phase 8: Observability
-
Metrics (
ApiService/Infrastructure/Observability/)playlist_list_requests_totalplaylist_create_requests_totalplaylist_get_requests_totalplaylist_update_requests_totalplaylist_delete_requests_totalplaylist_tracks_add_requests_totalplaylist_tracks_remove_requests_totalplaylist_reorder_requests_totalplaylist_track_count(histogram)
-
Logging
- Playlist operations with
PlaylistId,UserId,CorrelationId - Track additions/removals with count and position
- Playlist operations with
Phase 9: Testing
-
Unit Tests
PlaylistServiceTests- Position reindexing logic
- Quota enforcement
- Track validation
-
Integration Tests
- End-to-end CRUD flow
- Add/remove/reorder tracks
- Track deletion cascade to playlists
- Concurrent modification handling
Files to Create/Modify
New Files
| File | Purpose |
|---|---|
ApiService/Models/Playlist.cs | Playlist document model |
ApiService/Models/PlaylistTrackEntry.cs | Embedded track entry |
ApiService/Models/PlaylistVisibility.cs | Visibility enum |
ApiService/Configuration/PlaylistOptions.cs | Configuration |
ApiService/Services/IPlaylistService.cs | Service interface |
ApiService/Services/PlaylistService.cs | Service implementation |
ApiService/Endpoints/PlaylistEndpoints.cs | API endpoints |
ApiService/Models/PlaylistListQuery.cs | Query models |
ApiService/Models/PlaylistDetails.cs | Response DTOs |
ApiService/Infrastructure/Indexes/Playlists_ByUserForSearch.cs | Search index |
ApiService/Infrastructure/Indexes/Playlists_ByTrackReference.cs | Track reference index |
ApiService/Infrastructure/Exceptions/PlaylistExceptions.cs | Custom exceptions |
Modified Files
| File | Changes |
|---|---|
ApiService/Program.cs | Register services, rate limiting |
Workers.Lifecycle/PhysicalDeletionService.cs | Add playlist cleanup |
Stage 6 Documentation
Detailed specifications are available in doc/implementation/stage-6/:
| Document | Description |
|---|---|
00-overview.md | Architecture diagram and index |
01-data-model.md | Playlist and PlaylistTrackEntry models |
02-api-list-playlists.md | GET /playlists endpoint |
03-api-create-playlist.md | POST /playlists endpoint |
04-api-get-playlist.md | GET /playlists/{id} endpoint |
05-api-update-playlist.md | PATCH /playlists/{id} endpoint |
06-api-delete-playlist.md | DELETE /playlists/{id} endpoint |
07-api-add-tracks.md | POST /playlists/{id}/tracks endpoint |
08-api-remove-track.md | DELETE /playlists/{id}/tracks/{pos} endpoint |
09-api-reorder-tracks.md | POST /playlists/{id}/reorder endpoint |
10-service-interface.md | IPlaylistService and DTOs |
11-ravendb-indexes.md | Search and track reference indexes |
12-track-deletion-integration.md | Lifecycle worker integration |
13-configuration.md | PlaylistOptions configuration |
14-endpoint-implementation.md | PlaylistEndpoints.cs structure |
18-test-strategy.md | Unit and integration test plan |
19-implementation-tasks.md | Implementation checklist |
Related Skills
- add-api-endpoint - For endpoint structure
- add-cursor-pagination - For playlist list pagination
- add-ravendb-index - For creating RavenDB indexes
- add-rate-limiting - For rate limiting policies
- add-observability - For metrics and tracing
- add-playlist-reordering - For reorder implementation
- add-playlist-tracks - For track add/remove
Claude Agents
- playlist-api-implementer - Implement playlist service, endpoints, and models
- playlist-tester - Write unit and integration tests for playlists
Validation Checklist
- All CRUD endpoints return RFC 7807 problem details on error
- Rate limiting enforced on all mutation endpoints
- Playlist quota enforced (200 per user)
- Track limit enforced (10,000 per playlist)
- Track ownership verified before adding to playlist
- Deleted tracks not allowed in playlists
- Position indices maintained correctly
- Denormalized fields updated atomically
- Optimistic concurrency on updates
- Lifecycle worker removes deleted track references
- All operations logged with correlation ID