Skip to main content

Copyright Manager

Overview​

Copyright Manager lets streamers check a song's current status on demand, manage account safe and blocked lists, and import tracks from Spotify playlists. Community reports and recommendations create candidates for platform-operator review. A vote or report never adds a song to a global list automatically. The account-facing module is gated by the feature:copyright_detection feature flag. Background copyright checking is inactive because its worker is not started.

Architecture​

Backend​

  • GraphQL (apps/api/src/graphql/copyright.rs) -- Queries for safe songs, blocked songs, playlist syncs, song status checks, and vote candidates. Mutations for adding/deleting safe/blocked songs, importing playlists, managing playlist syncs, casting votes, and admin approval/dismissal of vote candidates.
  • Database (apps/api/src/db/copyright.rs) -- PostgreSQL operations for safe/blocked song lists, playlist sync records, song status checks (checks by spotify_track_id, ISRC, or title/artist fuzzy match), vote casting, and vote candidate aggregation.

Song Identification​

Each list query matches a supplied Spotify Track ID, ISRC, or case-insensitive title and artist combination. The lookup checks the safe list first, then the blocked list, then pending reports; the first matching list determines the status.

Frontend​

  • Copyright Manager dashboard with safe/blocked song lists.
  • Song status checker with on-demand lookup.
  • Spotify playlist import wizard.
  • Community voting interface.
  • Admin review panel for vote candidates.

API​

GraphQL Queries​

QueryPermissionDescription
safeSongscopyright:readList all safe songs for the account (includes global entries)
blockedSongscopyright:readList all blocked songs for the account (includes global entries)
playlistSyncscopyright:readList playlist sync records for the account
checkSong(spotifyTrackId?, isrc?, title?, artist?)copyright:readCheck copyright status of a song. Returns SongStatusResult (SAFE/BLOCKED/REPORTED/UNKNOWN).
voteCandidatescopyright:readList tracks with at least one pending community report; tallies count pending reports only.
adminReports(status?, reportType?, limit?, offset?)copyright:read - admin scope (AdminPermissionGuard)Paginated list of all-status reports from the account-less global copyright_reports table (ZAF-734)
curatedPlaylistTracks(playlistId: UUID!)copyright:readTracks for a curated playlist with song info
publicSafeSongs(search?, limit?, offset?)Feature gate onlyPublic: paginated global safe songs
publicBlockedSongs(search?, limit?, offset?)Feature gate onlyPublic: paginated global blocked songs
publicReports(limit?, offset?)Feature gate onlyPublic: pending copyright reports
publicSystemPlaylistsFeature gate onlyPublic: system-curated playlists
publicSystemPlaylist(id: UUID!)Feature gate onlyPublic: one system-curated playlist; null when unknown
publicSong(id: UUID!)Feature gate onlyPublic: one global safe or blocked song (same fields as GET /v1/songs/{id}); null when unknown or not global
publicSongSitemap(after?: UUID, first?: Int)Feature gate onlyPublic: global song ids for the sitemap, { items { id lastmod } hasMore }, keyset-paged on id (pass the last id as after), page size default and maximum 5000. lastmod is the song's created_at or null. Which lists are included (safe and blocked today) is a server constant, not an argument
publicSongStatus(spotifyTrackId?, isrc?, title?, artist?)Feature gate onlyPublic: global copyright status for a song

Public marketing surface gates​

The unauthenticated /music section on the marketing site (apps/web/src/app/(marketing)/music/*) is gated by two admin-scope kill-switch flags independent of the account-level feature:copyright_detection:

FlagGates
system:copyright_public_directoryThe entire /music surface - landing page, song/playlist browsers, detail pages, reports, and the nav links in header + footer. When off, every page under /music/* returns 404 and the nav links disappear.
system:copyright_public_quick_checkOnly the inline paste-a-URL quick-check widget on /music. The rest of the directory stays live when this is off.

Both flags resolve globally (no account context) via the GraphQL publicFeatureEnabled(key) resolver, called from apps/web/src/lib/public-feature.ts through serverGqlSSR and read server-side by the marketing layout at apps/web/src/app/(marketing)/layout.tsx. The matching REST endpoint GET /v1/features/enabled?key=... stays available for external SDK consumers (CLIs, bots, scripts) - frontend code routes through GraphQL to keep the "Frontend → Next.js proxy → GraphQL" pattern consistent. Admins toggle them in the feature flags admin page; changes propagate within ~5 minutes (in-memory cache in FeatureService).

GraphQL Mutations​

MutationPermissionDescription
addSafeSong(input: AddSafeSongInput)copyright:editAdd a song to the account's safe list
deleteSafeSong(id: UUID)copyright:deleteDelete a safe song (own account's only)
addBlockedSong(input: AddBlockedSongInput)copyright:editAdd a song to the account's blocked list
deleteBlockedSong(id: UUID)copyright:deleteDelete a blocked song (own account's only)
importPlaylist(spotifyPlaylistId)copyright:editImport tracks from a Spotify playlist as safe songs. Creates a playlist sync record.
deletePlaylistSync(id: UUID)copyright:deleteDelete a playlist sync and its associated safe songs
createCopyrightReport(input)copyright:reportReport a song as copyright-blocked
createSafeRecommendation(input)copyright:recommendRecommend a song as safe
confirmCopyrightReport(reportId: UUID!, input: ConfirmReportInput!)copyright:voteCast a confirmation vote on an existing copyright report
approveReport(id: UUID!, addToPlaylistId?: UUID)copyright:moderateApprove a report: copyright reports add to the global blocked list, recommendation reports add to the global safe list. Optionally add to a curated playlist.
dismissReport(id: UUID!)copyright:moderateDismiss a report without action
previewPlaylist(spotifyPlaylistId)copyright:editPreview a Spotify playlist's tracks with copyright status (10-minute Redis cache)
importSelectedSongs(input: ImportSelectedSongsInput!)copyright:editImport selected tracks from a previously previewed playlist. Returns ImportResult.
createCuratedPlaylist(genre, spotifyPlaylistName)copyright:edit - admin scope (AdminPermissionGuard)Admin: create a system-curated playlist (global copyright_curated_playlists table)
updateCuratedPlaylist(id, genre?, spotifyPlaylistName?)copyright:edit - admin scope (AdminPermissionGuard)Admin: update a curated playlist
deleteCuratedPlaylist(id)copyright:edit - admin scope (AdminPermissionGuard)Admin: delete a curated playlist
addToCuratedPlaylist(playlistId, safeSongId)copyright:edit - admin scope (AdminPermissionGuard)Admin: add a safe song to a curated playlist
removeFromCuratedPlaylist(playlistId, safeSongId)copyright:edit - admin scope (AdminPermissionGuard)Admin: remove a safe song from a curated playlist
syncCuratedPlaylist(id)copyright:edit - admin scope (AdminPermissionGuard)Admin: sync a curated playlist with Spotify (returns SyncResult)

REST Endpoints​

All paths live under /v1/copyright. Bodies are snake_case.

MethodPathPermissionDescription
GET/v1/copyright/checkcopyright:readCheck a song's status by spotify_track_id, isrc, or title+artist
GET/v1/copyright/safe-songscopyright:readList safe songs (account + globals)
POST/v1/copyright/safe-songscopyright:editAdd a safe song
DELETE/v1/copyright/safe-songs/{id}copyright:deleteDelete an account safe song
GET/v1/copyright/blocked-songscopyright:readList blocked songs (account + globals)
POST/v1/copyright/blocked-songscopyright:editAdd a blocked song
DELETE/v1/copyright/blocked-songs/{id}copyright:deleteDelete an account blocked song
POST/v1/copyright/import-playlistcopyright:editImport a Spotify playlist as safe songs
GET/v1/copyright/playlist-syncscopyright:readList playlist sync records
DELETE/v1/copyright/playlist-syncs/{id}copyright:deleteDelete a playlist sync (removes imported safe songs)
POST/v1/copyright/votecopyright:voteSubmit a vote for operator review; does not update a global list
GET/v1/copyright/vote-candidatescopyright:readList tracks with at least one pending community report; tallies count pending reports only
POST/v1/copyright/vote-candidates/{track_id}/approvecopyright:moderate (admin scope)Approve the majority verdict into the global blocked or safe list
POST/v1/copyright/vote-candidates/{track_id}/dismisscopyright:moderate (admin scope)Dismiss a candidate without changing either global list

The REST surface covers the account-facing song lists, playlist syncs, and voting. The curated-playlist management and playlist preview/selected-import are GraphQL-only.

Public REST endpoints​

The public directory reads have unauthenticated REST twins. Each runs the same fail-closed feature:copyright_detection check as the GraphQL public* queries (resolved for the caller's account when one is signed in, otherwise globally) and answers 403 FEATURE_DISABLED when it is off. Only global rows (account_id IS NULL) and system playlists are exposed.

MethodPathGraphQL twinDescription
GET/v1/songspublicSafeSongs / publicBlockedSongsPaginated global songs (page, limit ≤ 100, search, status, source)
GET/v1/songs/checkpublicSongStatusGlobal status of one song
GET/v1/songs/sitemappublicSongSitemapGlobal song ids for the sitemap (after, limit default and maximum 5000) → { items: [{ id, lastmod }], has_more }
GET/v1/songs/{id}publicSongOne global song; 404 when unknown
GET/v1/playlistspublicSystemPlaylistsSystem-curated playlists
GET/v1/playlists/{id}publicSystemPlaylistOne system-curated playlist with its tracks; 404 when unknown

Public music pages and sitemap​

The marketing directory links to individual /music/songs/\{id\} and /music/playlists/\{id\} pages. Each page reads its one record through publicSong(id) or publicSystemPlaylist(id) on the server; the page and its metadata share that lookup within the request. An unknown or malformed UUID, or a disabled music feature, gives a real 404. A failed backend read shows a retryable error page instead of treating the record as missing. Song status badges describe the community assessment, and the source label and disclaimer appear in the detail card. Playlist pages show the same disclaimer and link to Spotify.

The public sitemap lists global safe and blocked song detail URLs and system playlists with at least one track. It uses the paged publicSongSitemap query and publicSystemPlaylists, includes language alternates, and uses the song creation date or playlist update date when available. The music part is cached for one hour and is omitted if its read fails; the curated static routes remain listed. See SEO Foundation.

Input Types​

AddSafeSongInput:

FieldTypeDescription
songNameStringSong title
artistStringArtist name
spotifyTrackIdString?Spotify track ID
isrcString?International Standard Recording Code
sourceStringSource of the safe listing (e.g., "manual", "playlist_import")
sourceRefString?Reference to the source (e.g., playlist ID)

AddBlockedSongInput:

FieldTypeDescription
songNameStringSong title
artistStringArtist name
spotifyTrackIdString?Spotify track ID
isrcString?ISRC code
sourceStringSource of the blocked listing

Permissions​

PermissionDescription
copyright:readView safe/blocked song lists and check song status
copyright:editAdd safe/blocked songs, import playlists
copyright:deleteDelete safe/blocked songs, delete playlist syncs
copyright:reportSubmit copyright reports
copyright:recommendSubmit safe-song recommendations
copyright:voteConfirm existing copyright reports
copyright:moderateApprove or dismiss submitted reports (approveReport / dismissReport)

Every copyright query and mutation is additionally gated on the feature:copyright_detection feature flag. The five public* queries carry only that flag and no permission guard - they serve the unauthenticated /music directory.

Database​

TableDatabaseDescription
copyright_safe_songsPostgreSQLid, account_id (null for global), song_name, artist, spotify_track_id, isrc, source, source_ref, verified, created_at
copyright_blocked_songsPostgreSQLid, account_id (null for global), song_name, artist, spotify_track_id, isrc, source, auto_learned, created_at
copyright_playlist_syncsPostgreSQLid, account_id, spotify_playlist_id, spotify_playlist_name, auto_sync, last_synced_at, song_count, created_at
copyright_reportsPostgreSQLCommunity copyright reports and safe recommendations awaiting review

Song Status Check Logic​

The check_song_status function checks in order:

  1. Is the song in the blocked list (account-specific or global)?
  2. Is the song in the safe list (account-specific or global)?
  3. If neither, return Unknown.

Matching uses spotify_track_id first, then ISRC, then title+artist fuzzy match.

Data Flow​

Manual Song Management​

  1. User adds a song to their safe or blocked list via the dashboard.
  2. A client can request checkSong for the song.
  3. The system checks the safe list, blocked list, and pending reports in that order and returns the status.

Playlist Import​

  1. User provides a Spotify playlist ID.
  2. System fetches playlist tracks from the Spotify API.
  3. Tracks are bulk-inserted into copyright_safe_songs with source = "playlist_import".
  4. A copyright_playlist_syncs record is created for tracking.

Community Voting​

  1. Users submit reports (createCopyrightReport) or safe recommendations (createSafeRecommendation) for a song, and can confirm existing reports (confirmCopyrightReport).
  2. Submissions and confirmations remain moderation candidates; vote counts do not automatically add or remove global entries.
  3. Platform operators with the admin-scope copyright:moderate permission review candidates and either approveReport (a copyright report adds to the global blocked list, a recommendation report to the global safe list) or dismissReport. Approval and dismissal emit system-scope audit events.

Feature gating​

Copyright detection is gated by the feature:copyright_detection feature flag, resolved fail-closed through the account's plan (plan_features) with an optional per-account override. It is not a hard-coded plan constant - an admin can enable or disable it for a plan or a single account without a deploy.

Key Files​

PathDescription
apps/api/src/graphql/copyright.rsGraphQL queries and mutations
apps/api/src/routes/copyright.rsREST endpoints
apps/api/src/db/copyright.rsDatabase operations for safe/blocked songs, playlists, votes
apps/api/src/workers/copyright.rsCopyright checking worker implementation; no current startup caller