You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
Music/docs/superpowers/specs/2026-05-30-smart-playlist-c...

5.4 KiB

Smart Playlist Conditions — Design Spec

Date: 2026-05-30 Status: Approved

Overview

Extend smart playlists to support structured metadata-based conditions (e.g. artist = "Alicia Keys", year > 2015). Multiple conditions combine with AND. The existing FTS free-text smart playlist flow is preserved unchanged.

Data Model

SmartPlaylist (extended)

Add one new optional field to the existing SmartPlaylist struct:

var conditions: [SmartPlaylistCondition]?
  • nil → FTS mode (existing behavior, no change)
  • non-nil → structured SQL WHERE mode (new behavior)

SmartPlaylistCondition

struct SmartPlaylistCondition: Codable, Equatable, Sendable {
    var field: TrackField
    var op: ConditionOperator
    var value: ConditionValue
}

TrackField

String raw-value enum. Raw value matches the SQLite column name exactly (used directly in query building):

title, artist, albumArtist, album, genre, composer,
year, bpm, rating, playCount, trackNumber, discNumber,
duration, bitrate, sampleRate, fileSize,
dateAdded, dateModified, lastPlayedAt, fileFormat

Each field has an associated FieldType: .string, .int, .double, .date.

ConditionOperator

enum ConditionOperator: String, Codable {
    case equals
    case startsWith   // strings only
    case greaterThan  // numbers and dates only
    case lessThan     // numbers and dates only
}

Valid operators per field type:

  • String: equals, startsWith
  • Number (Int, Double): equals, greaterThan, lessThan
  • Date: equals, greaterThan, lessThan

ConditionValue

Tagged Codable union:

enum ConditionValue: Codable, Equatable, Sendable {
    case string(String)
    case int(Int)
    case double(Double)
    case date(Date)
}

Persistence

DB Migration (v5)

ALTER TABLE smart_playlists ADD COLUMN conditions TEXT;

Nullable, no default. Existing rows stay NULL (FTS mode).

Encoding

SmartPlaylist.conditions is encoded as a JSON string when writing to the conditions column and decoded on read. GRDB's Codable conformance handles this automatically via a custom columnEncodingStrategy or manual encode/decode.

Query Evaluation

SQL Generation

New private method in DatabaseService:

private func buildWhereClause(_ conditions: [SmartPlaylistCondition]) -> (sql: String, args: StatementArguments)

Mapping:

Field type Operator SQL fragment
String equals LOWER({col}) = LOWER(?)
String startsWith `LOWER({col}) LIKE LOWER(?)
Number equals {col} = ?
Number greaterThan {col} > ?
Number lessThan {col} < ?
Date equals {col} = ?
Date greaterThan {col} > ?
Date lessThan {col} < ?

Fragments joined with AND. Final query:

SELECT * FROM tracks WHERE <clause> ORDER BY <sortCol> COLLATE NOCASE <asc|desc>

fetchTracks branch

PlaylistViewModel.observeSmartPlaylistTracks branches on smartPlaylist.conditions:

  • nil → existing FTS ValueObservation (unchanged)
  • non-nil → new SQL WHERE ValueObservation using buildWhereClause

UI

Entry Point

New "New Smart Playlist…" item in the app menu (alongside existing "New Playlist…"). Triggers a sheet (not an alert) since the form has multiple fields.

Condition Builder Sheet

┌─────────────────────────────────────────┐
│  New Smart Playlist                      │
│                                          │
│  NAME                                    │
│  [________________________]              │
│                                          │
│  CONDITIONS (all must match)             │
│  [Field ▾] [Operator ▾] [Value    ] [−] │
│  [Field ▾] [Operator ▾] [Value    ] [−] │
│                                          │
│  [+ Add Condition          ]             │
│                                          │
│                    [Cancel]  [Save]      │
└─────────────────────────────────────────┘
  • One condition row shown by default
  • Operator picker options update when field changes (string → is/starts with; number/date → is/greater than/less than)
  • Value input adapts to field type: TextField for strings and numbers, DatePicker for date fields
  • Remove (−) button disabled when only one condition remains
  • Save disabled until name is non-empty and all condition values are non-empty

Edit Flow

  • Structured smart playlists: context menu shows "Edit…" → reopens the builder sheet pre-populated
  • FTS smart playlists: context menu keeps existing "Edit Search Query…" → existing text alert (no change)

Out of Scope

  • OR logic between conditions
  • Nested condition groups
  • Track limit per playlist ("limit to N songs")
  • Migrating existing FTS playlists to structured format
  • Live-updating toggle (not needed — ValueObservation already handles this automatically)