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.
166 lines
5.4 KiB
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)
|
|
|