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
| 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 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:read | 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 |
publicSystemPlaylist(id: UUID!) | Feature gate only | Public: one system-curated playlist; null when unknown |
publicSong(id: UUID!) | Feature gate only | Public: 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 only | Public: 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 only | Public: 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:
| Flag | Gates |
|---|---|
system:copyright_public_directory | The 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_check | Only 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
| 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(reportId: UUID!, input: ConfirmReportInput!) | copyright:vote | Cast a confirmation vote on an existing copyright report |
approveReport(id: UUID!, addToPlaylistId?: UUID) | copyright:moderate | 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: UUID!) | copyright:moderate | 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 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.
| 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 | Submit a vote for operator review; does not update a global list |
GET | /v1/copyright/vote-candidates | copyright:read | List tracks with at least one pending community report; tallies count pending reports only |
POST | /v1/copyright/vote-candidates/{track_id}/approve | copyright:moderate (admin scope) | Approve the majority verdict into the global blocked or safe list |
POST | /v1/copyright/vote-candidates/{track_id}/dismiss | copyright: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.
| Method | Path | GraphQL twin | Description |
|---|---|---|---|
GET | /v1/songs | publicSafeSongs / publicBlockedSongs | Paginated global songs (page, limit ≤ 100, search, status, source) |
GET | /v1/songs/check | publicSongStatus | Global status of one song |
GET | /v1/songs/sitemap | publicSongSitemap | Global song ids for the sitemap (after, limit default and maximum 5000) → { items: [{ id, lastmod }], has_more } |
GET | /v1/songs/{id} | publicSong | One global song; 404 when unknown |
GET | /v1/playlists | publicSystemPlaylists | System-curated playlists |
GET | /v1/playlists/{id} | publicSystemPlaylist | One 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:
| 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 |
copyright:moderate | Approve 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
| 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_reports | PostgreSQL | Community copyright reports and safe recommendations awaiting review |
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.
- A client can request
checkSongfor the song. - The system checks the safe list, blocked list, and pending reports in that order 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). - Submissions and confirmations remain moderation candidates; vote counts do not automatically add or remove global entries.
- Platform operators with the admin-scope
copyright:moderatepermission review candidates and eitherapproveReport(acopyrightreport adds to the global blocked list, arecommendationreport to the global safe list) ordismissReport. 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
| Path | Description |
|---|---|
apps/api/src/graphql/copyright.rs | GraphQL queries and mutations |
apps/api/src/routes/copyright.rs | REST endpoints |
apps/api/src/db/copyright.rs | Database operations for safe/blocked songs, playlists, votes |
apps/api/src/workers/copyright.rs | Copyright checking worker implementation; no current startup caller |