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, Description
Tracks (embedded List<PlaylistTrackEntry>)
TrackCount, TotalDuration (denormalized)
Visibility enum (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, PlaylistDetailQuery
PlaylistListItem, PlaylistDetails, PlaylistTrackItem
CreatePlaylistRequest, UpdatePlaylistRequest
AddTracksRequest, 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/)
PlaylistNotFoundException
PlaylistAccessDeniedException
PlaylistQuotaExceededException
PlaylistTrackLimitExceededException
PlaylistTrackNotFoundException
InvalidPositionException
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/min
playlist-create: 20 req/min
playlist-update: 30 req/min
playlist-delete: 20 req/min
playlist-tracks-add: 30 req/min
playlist-tracks-remove: 60 req/min
playlist-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_ByTrackReference index to find affected playlists
- Remove all entries for the deleted track
- Reindex positions
- Update denormalized
TrackCount and TotalDuration
Phase 8: Observability
Metrics (ApiService/Infrastructure/Observability/)
playlist_list_requests_total
playlist_create_requests_total
playlist_get_requests_total
playlist_update_requests_total
playlist_delete_requests_total
playlist_tracks_add_requests_total
playlist_tracks_remove_requests_total
playlist_reorder_requests_total
playlist_track_count (histogram)
Logging
- Playlist operations with
PlaylistId, UserId, CorrelationId
- Track additions/removals with count and position
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
1---2name: implement-playlists-23description: Plan and implement Stage 6 Playlists with CRUD endpoints, track management, and reordering (plan)4---5# Implement Playlists Skill67Plan and implement Stage 6 Playlists for NovaTune: CRUD endpoints, track management, stable ordering, and lifecycle integration.89## Overview1011Stage 6 implements playlist management with:12- **GET /playlists** - List playlists with search and cursor-based pagination13- **POST /playlists** - Create playlist with quota enforcement14- **GET /playlists/{playlistId}** - Get playlist with paginated tracks15- **PATCH /playlists/{playlistId}** - Update playlist metadata16- **DELETE /playlists/{playlistId}** - Hard delete playlist17- **POST /playlists/{playlistId}/tracks** - Add tracks at position18- **DELETE /playlists/{playlistId}/tracks/{position}** - Remove track19- **POST /playlists/{playlistId}/reorder** - Reorder tracks2021## Implementation Plan2223### Phase 1: Models and Configuration24251. **Create Playlist Model** (`ApiService/Models/Playlist.cs`)26 - `PlaylistId` (ULID)27 - `UserId` (owner)28 - `Name`, `Description`29 - `Tracks` (embedded `List<PlaylistTrackEntry>`)30 - `TrackCount`, `TotalDuration` (denormalized)31 - `Visibility` enum (Private, Unlisted, Public)32 - `CreatedAt`, `UpdatedAt`33342. **Create PlaylistTrackEntry** (`ApiService/Models/PlaylistTrackEntry.cs`)35 - `Position` (0-based index)36 - `TrackId` (ULID reference)37 - `AddedAt`38393. **Add Configuration** (`ApiService/Configuration/PlaylistOptions.cs`)40 - `MaxPlaylistsPerUser` (default: 200)41 - `MaxTracksPerPlaylist` (default: 10,000)42 - `MaxTracksPerAddRequest` (default: 100)43 - `MaxMovesPerReorderRequest` (default: 50)44 - `MaxNameLength` (default: 100)45 - `MaxDescriptionLength` (default: 500)46 - `DefaultPageSize` (default: 20)47 - `MaxPageSize` (default: 50)48494. **Add DTOs** (`ApiService/Models/`)50 - `PlaylistListQuery`, `PlaylistDetailQuery`51 - `PlaylistListItem`, `PlaylistDetails`, `PlaylistTrackItem`52 - `CreatePlaylistRequest`, `UpdatePlaylistRequest`53 - `AddTracksRequest`, `ReorderRequest`, `MoveOperation`5455### Phase 2: RavenDB Indexes56571. **Playlists_ByUserForSearch** (`ApiService/Infrastructure/Indexes/`)58 ```csharp59 Map = playlists => from playlist in playlists60 select new61 {62 playlist.UserId,63 playlist.Name,64 playlist.TrackCount,65 playlist.CreatedAt,66 playlist.UpdatedAt,67 SearchText = playlist.Name68 };69 Index("SearchText", FieldIndexing.Search);70 ```71722. **Playlists_ByTrackReference** (`ApiService/Infrastructure/Indexes/`)73 ```csharp74 Map = playlists => from playlist in playlists75 from track in playlist.Tracks76 select new77 {78 UserId = playlist.UserId,79 PlaylistId = playlist.PlaylistId,80 TrackId = track.TrackId81 };82 ```8384### Phase 3: Service Layer85861. **IPlaylistService** (`ApiService/Services/`)87 - `ListPlaylistsAsync(userId, query, ct)`88 - `CreatePlaylistAsync(userId, request, ct)`89 - `GetPlaylistAsync(playlistId, userId, query, ct)`90 - `UpdatePlaylistAsync(playlistId, userId, request, ct)`91 - `DeletePlaylistAsync(playlistId, userId, ct)`92 - `AddTracksAsync(playlistId, userId, request, ct)`93 - `RemoveTrackAsync(playlistId, userId, position, ct)`94 - `ReorderTracksAsync(playlistId, userId, request, ct)`95 - `RemoveDeletedTrackReferencesAsync(trackId, userId, ct)`96972. **Custom Exceptions** (`ApiService/Infrastructure/Exceptions/`)98 - `PlaylistNotFoundException`99 - `PlaylistAccessDeniedException`100 - `PlaylistQuotaExceededException`101 - `PlaylistTrackLimitExceededException`102 - `PlaylistTrackNotFoundException`103 - `InvalidPositionException`104105### Phase 4: API Endpoints1061071. **PlaylistEndpoints.cs** (`ApiService/Endpoints/`)108 ```csharp109 group.MapGet("/", HandleListPlaylists).RequireRateLimiting("playlist-list");110 group.MapPost("/", HandleCreatePlaylist).RequireRateLimiting("playlist-create");111 group.MapGet("/{playlistId}", HandleGetPlaylist);112 group.MapPatch("/{playlistId}", HandleUpdatePlaylist).RequireRateLimiting("playlist-update");113 group.MapDelete("/{playlistId}", HandleDeletePlaylist).RequireRateLimiting("playlist-delete");114 group.MapPost("/{playlistId}/tracks", HandleAddTracks).RequireRateLimiting("playlist-tracks-add");115 group.MapDelete("/{playlistId}/tracks/{position:int}", HandleRemoveTrack).RequireRateLimiting("playlist-tracks-remove");116 group.MapPost("/{playlistId}/reorder", HandleReorderTracks).RequireRateLimiting("playlist-reorder");117 ```1181192. **Rate Limiting Policies**120 - `playlist-list`: 60 req/min121 - `playlist-create`: 20 req/min122 - `playlist-update`: 30 req/min123 - `playlist-delete`: 20 req/min124 - `playlist-tracks-add`: 30 req/min125 - `playlist-tracks-remove`: 60 req/min126 - `playlist-reorder`: 30 req/min127128### Phase 5: Track Validation129130When adding tracks to playlists:1311. Verify track IDs are valid ULIDs1322. Verify tracks exist in RavenDB1333. Verify tracks are owned by the same user1344. Verify tracks are not deleted (`Status != Deleted`)1355. Verify playlist track limit not exceeded136137```csharp138var trackDocs = await _session.LoadAsync<Track>(139 request.TrackIds.Select(id => $"Tracks/{id}"), ct);140141foreach (var (trackId, track) in trackDocs)142{143 if (track is null)144 throw new TrackNotFoundException(trackId);145 if (track.UserId != userId)146 throw new TrackAccessDeniedException(trackId);147 if (track.Status == TrackStatus.Deleted)148 throw new TrackDeletedException(trackId);149}150```151152### Phase 6: Position Management153154**Adding tracks:**155```csharp156var insertPosition = request.Position ?? playlist.Tracks.Count;157158// Shift existing tracks159foreach (var entry in playlist.Tracks.Where(t => t.Position >= insertPosition))160 entry.Position += request.TrackIds.Count;161162// Add new tracks163var newEntries = request.TrackIds.Select((id, i) => new PlaylistTrackEntry164{165 Position = insertPosition + i,166 TrackId = id,167 AddedAt = now168});169playlist.Tracks.AddRange(newEntries);170```171172**Removing tracks:**173```csharp174playlist.Tracks.Remove(trackToRemove);175176// Reindex positions177foreach (var entry in playlist.Tracks.Where(t => t.Position > position))178 entry.Position--;179```180181**Reordering tracks:**182```csharp183foreach (var move in request.Moves)184{185 var track = tracks[move.From];186 tracks.RemoveAt(move.From);187 tracks.Insert(move.To, track);188}189190// Reassign positions191for (var i = 0; i < tracks.Count; i++)192 tracks[i].Position = i;193```194195### Phase 7: Lifecycle Integration196197Extend lifecycle worker to clean up playlist references when tracks are physically deleted:1981991. Query `Playlists_ByTrackReference` index to find affected playlists2002. Remove all entries for the deleted track2013. Reindex positions2024. Update denormalized `TrackCount` and `TotalDuration`203204### Phase 8: Observability2052061. **Metrics** (`ApiService/Infrastructure/Observability/`)207 - `playlist_list_requests_total`208 - `playlist_create_requests_total`209 - `playlist_get_requests_total`210 - `playlist_update_requests_total`211 - `playlist_delete_requests_total`212 - `playlist_tracks_add_requests_total`213 - `playlist_tracks_remove_requests_total`214 - `playlist_reorder_requests_total`215 - `playlist_track_count` (histogram)2162172. **Logging**218 - Playlist operations with `PlaylistId`, `UserId`, `CorrelationId`219 - Track additions/removals with count and position220221### Phase 9: Testing2222231. **Unit Tests**224 - `PlaylistServiceTests`225 - Position reindexing logic226 - Quota enforcement227 - Track validation2282292. **Integration Tests**230 - End-to-end CRUD flow231 - Add/remove/reorder tracks232 - Track deletion cascade to playlists233 - Concurrent modification handling234235## Files to Create/Modify236237### New Files238239| File | Purpose |240|------|---------|241| `ApiService/Models/Playlist.cs` | Playlist document model |242| `ApiService/Models/PlaylistTrackEntry.cs` | Embedded track entry |243| `ApiService/Models/PlaylistVisibility.cs` | Visibility enum |244| `ApiService/Configuration/PlaylistOptions.cs` | Configuration |245| `ApiService/Services/IPlaylistService.cs` | Service interface |246| `ApiService/Services/PlaylistService.cs` | Service implementation |247| `ApiService/Endpoints/PlaylistEndpoints.cs` | API endpoints |248| `ApiService/Models/PlaylistListQuery.cs` | Query models |249| `ApiService/Models/PlaylistDetails.cs` | Response DTOs |250| `ApiService/Infrastructure/Indexes/Playlists_ByUserForSearch.cs` | Search index |251| `ApiService/Infrastructure/Indexes/Playlists_ByTrackReference.cs` | Track reference index |252| `ApiService/Infrastructure/Exceptions/PlaylistExceptions.cs` | Custom exceptions |253254### Modified Files255256| File | Changes |257|------|---------|258| `ApiService/Program.cs` | Register services, rate limiting |259| `Workers.Lifecycle/PhysicalDeletionService.cs` | Add playlist cleanup |260261## Stage 6 Documentation262263Detailed specifications are available in `doc/implementation/stage-6/`:264265| Document | Description |266|----------|-------------|267| `00-overview.md` | Architecture diagram and index |268| `01-data-model.md` | Playlist and PlaylistTrackEntry models |269| `02-api-list-playlists.md` | GET /playlists endpoint |270| `03-api-create-playlist.md` | POST /playlists endpoint |271| `04-api-get-playlist.md` | GET /playlists/{id} endpoint |272| `05-api-update-playlist.md` | PATCH /playlists/{id} endpoint |273| `06-api-delete-playlist.md` | DELETE /playlists/{id} endpoint |274| `07-api-add-tracks.md` | POST /playlists/{id}/tracks endpoint |275| `08-api-remove-track.md` | DELETE /playlists/{id}/tracks/{pos} endpoint |276| `09-api-reorder-tracks.md` | POST /playlists/{id}/reorder endpoint |277| `10-service-interface.md` | IPlaylistService and DTOs |278| `11-ravendb-indexes.md` | Search and track reference indexes |279| `12-track-deletion-integration.md` | Lifecycle worker integration |280| `13-configuration.md` | PlaylistOptions configuration |281| `14-endpoint-implementation.md` | PlaylistEndpoints.cs structure |282| `18-test-strategy.md` | Unit and integration test plan |283| `19-implementation-tasks.md` | Implementation checklist |284285## Related Skills286287- **add-api-endpoint** - For endpoint structure288- **add-cursor-pagination** - For playlist list pagination289- **add-ravendb-index** - For creating RavenDB indexes290- **add-rate-limiting** - For rate limiting policies291- **add-observability** - For metrics and tracing292- **add-playlist-reordering** - For reorder implementation293- **add-playlist-tracks** - For track add/remove294295## Claude Agents296297- **playlist-api-implementer** - Implement playlist service, endpoints, and models298- **playlist-tester** - Write unit and integration tests for playlists299300## Validation Checklist301302- [ ] All CRUD endpoints return RFC 7807 problem details on error303- [ ] Rate limiting enforced on all mutation endpoints304- [ ] Playlist quota enforced (200 per user)305- [ ] Track limit enforced (10,000 per playlist)306- [ ] Track ownership verified before adding to playlist307- [ ] Deleted tracks not allowed in playlists308- [ ] Position indices maintained correctly309- [ ] Denormalized fields updated atomically310- [ ] Optimistic concurrency on updates311- [ ] Lifecycle worker removes deleted track references312- [ ] All operations logged with correlation ID