Skip to main content

Copyright Detection

Overview

The Copyright Detection module helps streamers identify whether songs are safe or blocked for streaming. It maintains per-account and global safe/blocked song lists, supports bulk importing tracks from Spotify playlists, provides a real-time song status checker, and includes a community voting system where users vote on a song's copyright status. Moderators can review vote candidates and approve them into the global blocked list. The whole module is gated by the feature:copyright_detection feature flag.

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

Songs are identified by multiple keys (checked in priority order):

  1. Spotify Track ID -- Exact match on spotify_track_id
  2. ISRC -- International Standard Recording Code match
  3. Title + Artist -- Fuzzy text match on song_name and artist

Frontend

  • Copyright detection dashboard with safe/blocked song lists.
  • Song status checker with real-time 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 songs with enough community votes/reports for admin review.
adminReports(status?, reportType?, limit?, offset?)copyright:readadmin 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
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:editadmin scope (AdminPermissionGuard)Admin: create a system-curated playlist (global copyright_curated_playlists table)
updateCuratedPlaylist(id, genre?, spotifyPlaylistName?)copyright:editadmin scope (AdminPermissionGuard)Admin: update a curated playlist
deleteCuratedPlaylist(id)copyright:editadmin scope (AdminPermissionGuard)Admin: delete a curated playlist
addToCuratedPlaylist(playlistId, safeSongId)copyright:editadmin scope (AdminPermissionGuard)Admin: add a safe song to a curated playlist
removeFromCuratedPlaylist(playlistId, safeSongId)copyright:editadmin scope (AdminPermissionGuard)Admin: remove a safe song from a curated playlist
syncCuratedPlaylist(id)copyright:editadmin 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:voteCast a vote on a song (confirm/recommend)
GET/v1/copyright/vote-candidatescopyright:readList candidates ready for admin review
POST/v1/copyright/vote-candidates/{track_id}/approvecopyright:moderateApprove a candidate into the global blocked list
POST/v1/copyright/vote-candidates/{track_id}/dismisscopyright:moderateDismiss a candidate without action

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

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_votesPostgreSQLid, user_id, spotify_track_id, song_name, artist, vote_type ("copyright" or "safe"), created_at

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. When checking a song during a stream, checkSong is called.
  3. The system checks blocked list first, then safe list, 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. Votes are aggregated. When enough accumulate, the song appears in voteCandidates.
  3. Holders of copyright:moderate review candidates and either approveReport (a copyright report adds to the global blocked list, a recommendation report to the global safe list) or dismissReport.

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.rsBackground copyright worker