# 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: ```swift var conditions: [SmartPlaylistCondition]? ``` - `nil` → FTS mode (existing behavior, no change) - non-nil → structured SQL WHERE mode (new behavior) ### SmartPlaylistCondition ```swift 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 ```swift 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: ```swift enum ConditionValue: Codable, Equatable, Sendable { case string(String) case int(Int) case double(Double) case date(Date) } ``` ## Persistence ### DB Migration (v5) ```sql 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`: ```swift 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: ```sql SELECT * FROM tracks WHERE ORDER BY COLLATE NOCASE ``` ### 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)