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 FTSValueObservation(unchanged)- non-nil → new SQL WHERE
ValueObservationusingbuildWhereClause
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:
TextFieldfor strings and numbers,DatePickerfor 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 —
ValueObservationalready handles this automatically)