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. Admins can review vote candidates and approve them into the global blocked list. This feature is available on Pro plan and above.
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):
- Spotify Track ID -- Exact match on
spotify_track_id - ISRC -- International Standard Recording Code match
- Title + Artist -- Fuzzy text match on
song_nameandartist
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
| Query | Permission | Description |
|---|---|---|
safeSongs | copyright:read | List all safe songs for the account (includes global entries) |
blockedSongs | copyright:read | List all blocked songs for the account (includes global entries) |
playlistSyncs | copyright:read | List playlist sync records for the account |
checkSong(spotifyTrackId?, isrc?, title?, artist?) | copyright:read | Check copyright status of a song. Returns SongStatusResult (SAFE/BLOCKED/REPORTED/UNKNOWN). |
voteCandidates | copyright:read | List songs with enough community votes/reports for admin review. |
adminReports(status?, reportType?, limit?, offset?) | copyright:edit | Admin: paginated list of reports with optional status/type filters |
curatedPlaylistTracks(playlistId) | copyright:edit | Admin: tracks for a curated playlist with song info |
publicSafeSongs(search?, limit?, offset?) | Feature gate only | Public: paginated global safe songs |
publicBlockedSongs(search?, limit?, offset?) | Feature gate only | Public: paginated global blocked songs |
publicReports(limit?, offset?) | Feature gate only | Public: pending copyright reports |
publicSystemPlaylists | Feature gate only | Public: system-curated playlists |
publicSongStatus(spotifyTrackId?, isrc?, title?, artist?) | Feature gate only | Public: global copyright status for a song |
GraphQL Mutations
| Mutation | Permission | Description |
|---|---|---|
addSafeSong(input: AddSafeSongInput) | copyright:edit | Add a song to the account's safe list |
deleteSafeSong(id: UUID) | copyright:delete | Delete a safe song (own account's only) |
addBlockedSong(input: AddBlockedSongInput) | copyright:edit | Add a song to the account's blocked list |
deleteBlockedSong(id: UUID) | copyright:delete | Delete a blocked song (own account's only) |
importPlaylist(spotifyPlaylistId) | copyright:edit | Import tracks from a Spotify playlist as safe songs. Creates a playlist sync record. |
deletePlaylistSync(id: UUID) | copyright:delete | Delete a playlist sync and its associated safe songs |
createCopyrightReport(input) | copyright:report | Report a song as copyright-blocked |
createSafeRecommendation(input) | copyright:recommend | Recommend a song as safe |
confirmCopyrightReport(id) | copyright:vote | Cast a confirmation vote on an existing copyright report |
approveReport(id, addToPlaylistId?) | copyright:edit | Approve 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) | copyright:edit | Dismiss a report without action |
previewPlaylist(spotifyPlaylistId) | copyright:edit | Preview a Spotify playlist's tracks with copyright status (10-minute Redis cache) |
importSelectedSongs(input: ImportSelectedSongsInput!) | copyright:edit | Import selected tracks from a previously previewed playlist. Returns ImportResult. |
createCuratedPlaylist(genre, spotifyPlaylistName) | copyright:edit | Admin: create a system-curated playlist |
updateCuratedPlaylist(id, genre?, spotifyPlaylistName?) | copyright:edit | Admin: update a curated playlist |
deleteCuratedPlaylist(id) | copyright:edit | Admin: delete a curated playlist |
addToCuratedPlaylist(playlistId, safeSongId) | copyright:edit | Admin: add a safe song to a curated playlist |
removeFromCuratedPlaylist(playlistId, safeSongId) | copyright:edit | Admin: remove a safe song from a curated playlist |
syncCuratedPlaylist(id) | copyright:edit | Admin: sync a curated playlist with Spotify (returns SyncResult) |
REST Endpoints
All paths live under /v1/copyright. Bodies are snake_case.
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/copyright/check | copyright:read | Check a song's status by spotify_track_id, isrc, or title+artist |
GET | /v1/copyright/safe-songs | copyright:read | List safe songs (account + globals) |
POST | /v1/copyright/safe-songs | copyright:edit | Add a safe song |
DELETE | /v1/copyright/safe-songs/{id} | copyright:delete | Delete an account safe song |
GET | /v1/copyright/blocked-songs | copyright:read | List blocked songs (account + globals) |
POST | /v1/copyright/blocked-songs | copyright:edit | Add a blocked song |
DELETE | /v1/copyright/blocked-songs/{id} | copyright:delete | Delete an account blocked song |
POST | /v1/copyright/import-playlist | copyright:edit | Import a Spotify playlist as safe songs |
GET | /v1/copyright/playlist-syncs | copyright:read | List playlist sync records |
DELETE | /v1/copyright/playlist-syncs/{id} | copyright:delete | Delete a playlist sync (removes imported safe songs) |
POST | /v1/copyright/vote | copyright:vote | Cast a vote on a song (confirm/recommend) |
GET | /v1/copyright/vote-candidates | copyright:read | List candidates ready for admin review |
POST | /v1/copyright/vote-candidates/{track_id}/approve | copyright:edit | Approve a candidate into the global blocked list |
POST | /v1/copyright/vote-candidates/{track_id}/dismiss | copyright:edit | Dismiss a candidate without action |
Input Types
AddSafeSongInput:
| Field | Type | Description |
|---|---|---|
songName | String | Song title |
artist | String | Artist name |
spotifyTrackId | String? | Spotify track ID |
isrc | String? | International Standard Recording Code |
source | String | Source of the safe listing (e.g., "manual", "playlist_import") |
sourceRef | String? | Reference to the source (e.g., playlist ID) |
AddBlockedSongInput:
| Field | Type | Description |
|---|---|---|
songName | String | Song title |
artist | String | Artist name |
spotifyTrackId | String? | Spotify track ID |
isrc | String? | ISRC code |
source | String | Source of the blocked listing |
Permissions
| Permission | Description |
|---|---|
copyright:read | View safe/blocked song lists and check song status |
copyright:edit | Add safe/blocked songs, import playlists |
copyright:delete | Delete safe/blocked songs, delete playlist syncs |
copyright:report | Submit copyright reports |
copyright:recommend | Submit safe-song recommendations |
copyright:vote | Confirm existing copyright reports |
Database
| Table | Database | Description |
|---|---|---|
copyright_safe_songs | PostgreSQL | id, account_id (null for global), song_name, artist, spotify_track_id, isrc, source, source_ref, verified, created_at |
copyright_blocked_songs | PostgreSQL | id, account_id (null for global), song_name, artist, spotify_track_id, isrc, source, auto_learned, created_at |
copyright_playlist_syncs | PostgreSQL | id, account_id, spotify_playlist_id, spotify_playlist_name, auto_sync, last_synced_at, song_count, created_at |
copyright_votes | PostgreSQL | id, 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:
- Is the song in the blocked list (account-specific or global)?
- Is the song in the safe list (account-specific or global)?
- If neither, return
Unknown.
Matching uses spotify_track_id first, then ISRC, then title+artist fuzzy match.
Data Flow
Manual Song Management
- User adds a song to their safe or blocked list via the dashboard.
- When checking a song during a stream,
checkSongis called. - The system checks blocked list first, then safe list, and returns the status.
Playlist Import
- User provides a Spotify playlist ID.
- System fetches playlist tracks from the Spotify API.
- Tracks are bulk-inserted into
copyright_safe_songswith source = "playlist_import". - A
copyright_playlist_syncsrecord is created for tracking.
Community Voting
- Users submit reports (
createCopyrightReport) or safe recommendations (createSafeRecommendation) for a song, and can confirm existing reports (confirmCopyrightReport). - Votes are aggregated. When enough accumulate, the song appears in
voteCandidates. - Admins review candidates and either
approveReport(adds to global blocked list) ordismissReport.
Plan Restriction
Copyright detection is only available on the Pro plan and above (copyright_detection_allowed in PlanLimits).
Key Files
| Path | Description |
|---|---|
apps/api/src/graphql/copyright.rs | GraphQL queries and mutations |
apps/api/src/db/copyright.rs | Database operations for safe/blocked songs, playlists, votes |