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...

166 lines
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:
```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 <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)