Skip to main content

Curated Playlists

Curated Playlists are system-managed Spotify playlists of verified copyright-safe songs, grouped by genre (lofi, edm, ncs, …). Lumio streamers can pull from them via the end-user safe-songs UI. The admin view is where operators create, edit, delete, and sync these playlists with Spotify.

Where to find it

Admin sidebar → Curated Playlists (/copyright-playlists). Detail pages live at /copyright-playlists/\{id\}.

Quick start

  1. Open Admin → Curated Playlists.
  2. Click Create Playlist, pick a genre slug (e.g. lofi), give it a name (e.g. Lumio Safe - Lo-Fi), and submit.
  3. Click the row to open the detail page.
  4. Use the search bar to find safe songs already in the catalog and click Add to append tracks to the playlist.
  5. Click Sync Now to push the playlist to Spotify (or re-pull metadata). A success toast confirms the sync.

Detailed walkthrough

Columns:

  • Genre — slug, rendered as an outline badge.
  • Playlist Name — display name; clicking it opens the detail page.
  • Tracks — count.
  • Spotify — external-link icon to open.spotify.com/playlist/…, shown only once the playlist has a Spotify playlist ID.
  • Last Updated — last edit / sync date.
  • Actions — icon buttons in this order: Edit (pencil), Sync (refresh, spins while running), Delete (trash).

The card header reads Curated Playlists (\{N\}) and carries the Create Playlist button. None of the row actions are permission-gated in the UI — the enforcement is server-side.

Create / Edit dialog fields:

  • Genre (placeholder: "e.g. lofi, edm, ncs...").
  • Playlist Name (placeholder: "Lumio Safe - Lo-Fi").

Two panels:

  1. Tracks — the current track list for this playlist. Each row has a Remove button.
  2. Safe Songs Search — lookup against the global safe-songs catalog with search box. Each match has an Add button.

Sync Now triggers a server-side reconciliation with Spotify, returning how many tracks were added and removed. After success the UI shows "Sync completed successfully."

Sync semantics

Sync pushes the track list to the linked Spotify playlist via Spotify's API. If the playlist isn't linked yet (missing Spotify playlist ID), sync is a no-op until an operator runs the underlying import flow. Failed syncs surface an error toast and leave the local list intact.

Common scenarios

  • Launch a new genre playlist — create, populate with 20–50 safe songs, sync once, share the Spotify link.
  • Track request from a streamer — search the catalog; if the track exists, add it; if not, approve it first via Copyright Recommendations so it enters the safe-songs table.
  • Remove a track that turned out to be unsafe — click Remove on the detail page, then go to Copyright Reports to mark the track unsafe globally.
  • Weekly sync — ops routine: open each playlist, click Sync Now.

Permissions

Curated playlists are an account-less global table (copyright_curated_playlists), so their management is enforced at admin (system) scope, not account scope.

ActionPermission
Create / edit / delete / sync a playlist, add / remove trackscopyright:editadmin scope (AdminPermissionGuard)
List a playlist's tracks (curatedPlaylistTracks)copyright:read
List playlists (publicSystemPlaylists)none — public

Every curated-playlist mutation — including delete and sync — is guarded by the admin-only AdminPermissionGuard("copyright:edit"); an account-level copyright:edit grant does not reach this surface (ZAF-734). copyright:delete exists but guards the safe-song / blocked-song / playlist-sync entries, not curated playlists. The curatedPlaylistTracks read stays on copyright:read because the identical rows are also served unauthenticated at GET /v1/playlists/\{id\}, so it exposes nothing new.

Dashboard entry requires admin:access. System admins hold every copyright permission at admin scope.

API

Curated playlists are a GraphQL-only surface. There are no /v1/copyright/playlists* REST endpoints; the paths below are the admin app's own Next.js proxy routes, which execute the GraphQL operations in the middle column.

UI actionGraphQLAdmin proxy route
List playlistspublicSystemPlaylistsGET /api/copyright/playlists
Create playlistcreateCuratedPlaylist(genre, spotifyPlaylistName)POST /api/copyright/playlists
Edit playlistupdateCuratedPlaylist(id, genre, spotifyPlaylistName)PATCH /api/copyright/playlists/\{id\}
Delete playlistdeleteCuratedPlaylist(id)DELETE /api/copyright/playlists/\{id\}
Sync playlistsyncCuratedPlaylist(id)POST /api/copyright/playlists/\{id\}/sync
List tracks in playlistcuratedPlaylistTracks(playlistId)GET /api/copyright/playlists/\{id\}/tracks
Search safe songspublicSafeSongs(search, limit, offset)GET /api/copyright/safe-songs?search=...
Add track to playlistaddToCuratedPlaylist(playlistId, safeSongId)POST /api/copyright/playlists/\{id\}/tracks
Remove track from playlistremoveFromCuratedPlaylist(playlistId, safeSongId)DELETE /api/copyright/playlists/\{id\}/tracks/\{song_id\}

A separate public, unauthenticated REST surface lists the same playlists for the marketing site: GET /v1/playlists and GET /v1/playlists/\{id\}.

Tips & gotchas

  • The genre field is free text (placeholder: "e.g. lofi, edm, ncs..."), not a validated slug. lo-fi and lofi are different values — pick one convention and stick with it.
  • Sync Now is idempotent but not instant; it can take a few seconds on a large playlist.
  • Delete is permanent and removes the mapping only — the Spotify playlist itself is not deleted.
  • The safe-songs catalog is shared with Copyright Recommendations; approving a recommendation there is how new candidate tracks enter the pool.
  • Delete uses a native browser confirm: "Are you sure you want to delete this playlist?"