14 KiB
Playlist Selective Download Design
Goal
Extend the catalogsync operations console so operators can download songs by selected playlists instead of relying on uncontrolled full-library download runs.
The new flow must allow the operator to:
- browse playlists through a paginated playlist-pool page
- filter playlists by download state
- select playlists on the current page
- run either
download already-synced songsorsync then downloadfor the selected playlists - persist a separate
wanted for downloadmarker for playlists that should remain in a long-term queue
This design keeps the existing job system and downloader intact wherever possible.
Scope
In Scope
- Upgrade
/playlistsfrom a read-only list into a playlist-pool management page - Add server-side pagination to the playlist page
- Add playlist filtering by:
- platform
- pool kind
- keyword
- download state
- wanted marker
- Add current-page checkbox selection and current-page select-all
- Add bulk actions:
下载已同步所选歌单同步后下载所选歌单加入待下载清单移出待下载清单
- Add a persistent playlist-level preference table for the wanted marker
- Reuse existing
download_onlyandsync_downloadjobs by passingplaylist_scope.playlist_ids - Compute playlist download state from live catalog and runner data
Out of Scope
- Cross-page remembered temporary selection
- Saved named selection sets
- Manual editing of computed playlist download state
- New downloader semantics outside the existing job system
- Per-playlist download history pages
- Automatic cancellation or reprioritization of in-flight jobs in this design
User Decisions Captured
This design encodes the following confirmed product decisions:
- Playlist download state uses multiple states instead of a simple downloaded flag
- The operator needs both:
- temporary current-page selection for immediate actions
- persistent playlist-level wanted markers
- If a song appears in multiple playlists, it counts as downloaded for all of them once the same
song_idhas an active local file - Playlists with no synced
playlist_songsmust show a dedicated未同步state - The playlist page must support pagination, page-level select-all, and download-state filtering
下载中is shown only when there is active running download work for songs belonging to that playlist- The state filter set is:
全部未同步未下载下载中部分已下载已下载
Constraints
- Existing
catalogsyncjob queue remains the only execution path - Only one active job still runs at a time
- Existing
download_onlyandsync_downloadjob types should remain valid and reusable - SQLite remains the backing store
- The first version should optimize for operational clarity and low migration risk over advanced UX
- The implementation should avoid full-library recomputation for every playlist page load because the NAS dataset is already large
Existing System Reuse
The current codebase already provides two critical capabilities that should be reused instead of reinvented:
playlist_scope.playlist_idsalready exists on jobs- download planning already supports filtering by
playlist_ids
Relevant current behavior:
download_onlyis already a first-class job typesync_downloadis already a first-class job typeOpsRunneralready resolvesplaylist_scope.playlist_idsDownloadPlanner.build_download_queue()already acceptsplaylist_idsCatalogRepository.list_pending_download_songs()already supportsplaylist_ids
Because of this, the main work is playlist management UI, playlist-state aggregation, and lightweight playlist preference persistence.
Recommended Approach
Use a mixed model:
- compute playlist state live for the current result page
- persist only playlist-level operator intent (
wanted for download) - use current bulk selections only as transient request payload
Why This Approach
This approach avoids two bad extremes:
- Pure runtime-only UI would lose long-term operator intent such as a curated wanted list
- Full cached playlist-state tables would add a large invalidation burden after every sync, download, retry, or file-state change
The mixed approach gives:
- correct and current state for the visible page
- minimal schema change
- low-risk reuse of the current pipeline
Data Model
New Table: playlist_download_preferences
Purpose:
- persist operator intent for playlists that should stay in a long-term wanted queue
Recommended fields:
playlist_id INTEGER PRIMARY KEY
is_wanted INTEGER NOT NULL DEFAULT 1
marked_by TEXT
created_at TEXT DEFAULT CURRENT_TIMESTAMP
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
Notes:
- use one row per playlist, not an event log
playlist_idshould be unique and serve as the primary key- deleting or setting
is_wanted = 0are both acceptable implementation choices; prefer explicit row persistence only if it simplifies auditing
No Cached Playlist State Table
Do not add a second table that stores computed playlist state such as 已下载 / 未下载 / 部分已下载.
Reason:
- those values depend on current
playlist_songs - current local file availability can change
- current running download items can change
The state should therefore be computed from live data for the current page.
Playlist State Model
For each playlist row, calculate:
song_countdownloaded_song_countrunning_download_song_countis_wanted
State Rules
未同步song_count = 0
下载中song_count > 0running_download_song_count > 0
未下载song_count > 0downloaded_song_count = 0running_download_song_count = 0
部分已下载song_count > 00 < downloaded_song_count < song_countrunning_download_song_count = 0
已下载song_count > 0downloaded_song_count = song_countrunning_download_song_count = 0
Downloaded Song Counting Rule
For one playlist song entry, the song is treated as downloaded if:
- the same
song_idhas an active local file location
It does not matter which playlist originally triggered that file download.
Running Download Counting Rule
For one playlist song entry, the song is treated as currently downloading if:
- there is a
runningjob item in stagedownload - that item points to the same
song_id
Queued-but-not-running work does not count as 下载中.
Playlist Page Design
URL
Keep the main page at:
/playlists
Query Parameters
Support these server-side filters:
pagepage_sizeplatformpool_kindstatuskeywordwanted_only
Default Pagination
Recommended defaults:
- default
page_size = 50 - allow
20 / 50 / 100
Reason:
- current NAS data already contains more than ten thousand playlists
- a fixed
LIMIT 500list will become increasingly unusable
Table Columns
Recommended visible columns:
- checkbox
- playlist id
- platform
- remote playlist id
- playlist name
- pool names
- song count
- downloaded song count
- computed state
- wanted marker
- updated at
Toolbar Actions
Recommended top-toolbar controls:
- platform filter
- pool-kind filter
- state filter
- keyword search
- wanted-only filter
- page-size selector
Bulk Action Groups
Temporary Selection Actions
Apply only to the currently selected playlist ids:
下载已同步所选歌单同步后下载所选歌单
Persistent Marker Actions
Apply only to the currently selected playlist ids:
加入待下载清单移出待下载清单
Selection Behavior
The first version should support only current-page temporary selection.
Rules:
全选本页selects all rows visible on the current page- changing page clears temporary selection
- filters changing the result page clear temporary selection
- persistent wanted markers remain stored independently of temporary selection
This keeps implementation simple and predictable.
API Design
GET /api/playlists
Purpose:
- return one filtered page of playlist rows with computed state
Request parameters:
pagepage_sizeplatformpool_kindstatuskeywordwanted_only
Response shape:
{
"items": [
{
"id": 123,
"platform": "qq",
"remote_playlist_id": "456",
"name": "Example Playlist",
"pool_names": "QQ 音乐歌单广场",
"song_count": 120,
"downloaded_song_count": 80,
"state": "部分已下载",
"is_wanted": true,
"updated_at": "2026-04-17 00:00:00"
}
],
"page": 1,
"page_size": 50,
"total": 12345
}
POST /api/playlists/mark-wanted
Purpose:
- persist wanted markers for the specified playlists
Request body:
{
"playlist_ids": [1, 2, 3],
"marked_by": "ops-console"
}
POST /api/playlists/unmark-wanted
Purpose:
- remove or disable wanted markers for the specified playlists
Request body:
{
"playlist_ids": [1, 2, 3]
}
POST /api/playlists/download
Purpose:
- create a
download_onlyjob scoped to selected playlist ids
Request body:
{
"playlist_ids": [1, 2, 3],
"requested_by": "ops-console"
}
Behavior:
- create one
download_onlyjob - store
playlist_scope.playlist_ids = [...] - do not include playlists that are not selected
POST /api/playlists/sync-download
Purpose:
- create a
sync_downloadjob scoped to selected playlist ids
Request body:
{
"playlist_ids": [1, 2, 3],
"requested_by": "ops-console"
}
Behavior:
- create one
sync_downloadjob - store
playlist_scope.playlist_ids = [...]
Interaction Rules
下载已同步所选歌单
This action should:
- create a
download_onlyjob for the selectedplaylist_ids - operate only on songs already present in
playlist_songs
Playlists in state 未同步 contribute no songs and therefore effectively produce no download work.
The UI should make this explicit instead of pretending those playlists are downloading.
同步后下载所选歌单
This action should:
- create a
sync_downloadjob for the selectedplaylist_ids - sync playlist songs first
- then download only missing songs from those playlists
Wanted Marker UX
The wanted marker is not itself a download state.
It is a separate operator-intent flag.
A playlist may therefore be:
已下载and still marked wanted未同步and marked wanted部分已下载and not marked wanted
This separation avoids overloading one column with two different meanings.
Query and Aggregation Strategy
Page-First Aggregation
Do not compute states for the whole library on each request.
Instead:
- query only the playlist ids for the requested page
- run aggregation queries only for those playlist ids
- merge the counts into the returned rows
This keeps response cost proportional to current page size instead of full library size.
Aggregations Needed Per Page
For the current page playlist ids:
- playlist song totals from
playlist_songs - downloaded song totals from:
playlist_songsfile_assetsfile_locationsstorage_backends
- running download song totals from:
playlist_songsjob_itemsjob_stages
- wanted markers from
playlist_download_preferences
Index Expectations
Add or verify indexes for:
pool_playlists(playlist_id)pool_playlists(pool_id)playlist_songs(playlist_id)playlist_songs(song_id)file_assets(song_id)file_locations(file_asset_id, status)job_items(song_id, status)job_stages(id, stage_type)playlist_download_preferences(playlist_id)playlist_download_preferences(is_wanted)
Error Handling
Empty Selection
Bulk actions should reject empty playlist_ids with a validation error.
Unknown Playlist IDs
If unknown ids are passed:
- ignore ids that do not exist
- fail only if the final valid set is empty
Duplicate Playlist IDs
Normalize to unique ids before processing.
Large Selection on One Page
The selected ids are page-scoped and therefore bounded by page_size.
This makes bulk requests predictable and low risk.
Testing Strategy
Repository and Query Tests
Add tests for:
- listing one playlist page with filters
- correct
totalcount under filtering - wanted marker persistence
- state aggregation across:
未同步未下载下载中部分已下载已下载
API Tests
Add tests for:
GET /api/playlistsPOST /api/playlists/mark-wantedPOST /api/playlists/unmark-wantedPOST /api/playlists/downloadPOST /api/playlists/sync-download
Verify that:
- created jobs use the expected job type
playlist_scope.playlist_idsis stored correctly- invalid or empty selection is rejected
UI Tests
At minimum, validate rendered page content and form wiring for:
- pagination controls
- state filter controls
- current-page select-all
- bulk action forms
- wanted-only filter
Regression Coverage
Keep existing download_only and sync_download behavior valid for callers outside the playlist page.
Rollout Notes
The recommended rollout order is:
- add the playlist preference table and repository helpers
- add page-level playlist listing API with computed state
- upgrade
/playlistsUI to pagination, filters, selection, and actions - add bulk job-creation endpoints for selected playlists
- verify on NAS with a controlled subset of playlists before using it for wide library download
Result
After this design lands, the operator workflow becomes:
- open
/playlists - filter to
未同步,未下载, or部分已下载 - select playlists on the current page
- choose either:
下载已同步所选歌单同步后下载所选歌单加入待下载清单
- observe progress through the existing jobs and worker views
This changes playlist download from an uncontrolled whole-library operation into a scoped, inspectable, operator-driven workflow.