Files
musicdl-catalog-sync-suite/catalog-sync/docs/superpowers/specs/2026-04-17-playlist-selective-download-design.md

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 songs or sync then download for the selected playlists
  • persist a separate wanted for download marker 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 /playlists from 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_only and sync_download jobs by passing playlist_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_id has an active local file
  • Playlists with no synced playlist_songs must 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 catalogsync job queue remains the only execution path
  • Only one active job still runs at a time
  • Existing download_only and sync_download job 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:

  1. playlist_scope.playlist_ids already exists on jobs
  2. download planning already supports filtering by playlist_ids

Relevant current behavior:

  • download_only is already a first-class job type
  • sync_download is already a first-class job type
  • OpsRunner already resolves playlist_scope.playlist_ids
  • DownloadPlanner.build_download_queue() already accepts playlist_ids
  • CatalogRepository.list_pending_download_songs() already supports playlist_ids

Because of this, the main work is playlist management UI, playlist-state aggregation, and lightweight playlist preference persistence.

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_id should be unique and serve as the primary key
  • deleting or setting is_wanted = 0 are 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_count
  • downloaded_song_count
  • running_download_song_count
  • is_wanted

State Rules

  • 未同步
    • song_count = 0
  • 下载中
    • song_count > 0
    • running_download_song_count > 0
  • 未下载
    • song_count > 0
    • downloaded_song_count = 0
    • running_download_song_count = 0
  • 部分已下载
    • song_count > 0
    • 0 < downloaded_song_count < song_count
    • running_download_song_count = 0
  • 已下载
    • song_count > 0
    • downloaded_song_count = song_count
    • running_download_song_count = 0

Downloaded Song Counting Rule

For one playlist song entry, the song is treated as downloaded if:

  • the same song_id has 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 running job item in stage download
  • 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:

  • page
  • page_size
  • platform
  • pool_kind
  • status
  • keyword
  • wanted_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 500 list 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:

  • page
  • page_size
  • platform
  • pool_kind
  • status
  • keyword
  • wanted_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_only job scoped to selected playlist ids

Request body:

{
  "playlist_ids": [1, 2, 3],
  "requested_by": "ops-console"
}

Behavior:

  • create one download_only job
  • store playlist_scope.playlist_ids = [...]
  • do not include playlists that are not selected

POST /api/playlists/sync-download

Purpose:

  • create a sync_download job scoped to selected playlist ids

Request body:

{
  "playlist_ids": [1, 2, 3],
  "requested_by": "ops-console"
}

Behavior:

  • create one sync_download job
  • store playlist_scope.playlist_ids = [...]

Interaction Rules

下载已同步所选歌单

This action should:

  • create a download_only job for the selected playlist_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_download job for the selected playlist_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:

  1. query only the playlist ids for the requested page
  2. run aggregation queries only for those playlist ids
  3. 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_songs
    • file_assets
    • file_locations
    • storage_backends
  • running download song totals from:
    • playlist_songs
    • job_items
    • job_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 total count under filtering
  • wanted marker persistence
  • state aggregation across:
    • 未同步
    • 未下载
    • 下载中
    • 部分已下载
    • 已下载

API Tests

Add tests for:

  • GET /api/playlists
  • POST /api/playlists/mark-wanted
  • POST /api/playlists/unmark-wanted
  • POST /api/playlists/download
  • POST /api/playlists/sync-download

Verify that:

  • created jobs use the expected job type
  • playlist_scope.playlist_ids is 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:

  1. add the playlist preference table and repository helpers
  2. add page-level playlist listing API with computed state
  3. upgrade /playlists UI to pagination, filters, selection, and actions
  4. add bulk job-creation endpoints for selected playlists
  5. 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:

  1. open /playlists
  2. filter to 未同步, 未下载, or 部分已下载
  3. select playlists on the current page
  4. choose either:
    • 下载已同步所选歌单
    • 同步后下载所选歌单
    • 加入待下载清单
  5. 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.