Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>feat/music-streaming
parent
6f07349a1e
commit
bebd30974a
@ -0,0 +1,152 @@ |
|||||||
|
import Foundation |
||||||
|
|
||||||
|
// Classifies a track field for operator and UI purposes. |
||||||
|
enum FieldType { |
||||||
|
case string, int, double, date |
||||||
|
} |
||||||
|
|
||||||
|
// Represents a track column that can be filtered on. |
||||||
|
// Raw value matches the SQLite column name in the "tracks" table. |
||||||
|
enum TrackField: String, Codable, CaseIterable, Identifiable, Sendable { |
||||||
|
case title, artist, albumArtist, album, genre, composer, fileFormat |
||||||
|
case year, bpm, rating, playCount, trackNumber, discNumber, bitrate, sampleRate |
||||||
|
case fileSize, duration |
||||||
|
case dateAdded, dateModified, lastPlayedAt |
||||||
|
|
||||||
|
var id: String { rawValue } |
||||||
|
|
||||||
|
var displayName: String { |
||||||
|
switch self { |
||||||
|
case .title: return "Title" |
||||||
|
case .artist: return "Artist" |
||||||
|
case .albumArtist: return "Album Artist" |
||||||
|
case .album: return "Album" |
||||||
|
case .genre: return "Genre" |
||||||
|
case .composer: return "Composer" |
||||||
|
case .fileFormat: return "File Format" |
||||||
|
case .year: return "Year" |
||||||
|
case .bpm: return "BPM" |
||||||
|
case .rating: return "Rating" |
||||||
|
case .playCount: return "Play Count" |
||||||
|
case .trackNumber: return "Track Number" |
||||||
|
case .discNumber: return "Disc Number" |
||||||
|
case .bitrate: return "Bitrate" |
||||||
|
case .sampleRate: return "Sample Rate" |
||||||
|
case .fileSize: return "File Size" |
||||||
|
case .duration: return "Duration" |
||||||
|
case .dateAdded: return "Date Added" |
||||||
|
case .dateModified: return "Date Modified" |
||||||
|
case .lastPlayedAt: return "Last Played" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var fieldType: FieldType { |
||||||
|
switch self { |
||||||
|
case .title, .artist, .albumArtist, .album, .genre, .composer, .fileFormat: |
||||||
|
return .string |
||||||
|
case .year, .bpm, .rating, .playCount, .trackNumber, .discNumber, .bitrate, .sampleRate, .fileSize: |
||||||
|
return .int |
||||||
|
case .duration: |
||||||
|
return .double |
||||||
|
case .dateAdded, .dateModified, .lastPlayedAt: |
||||||
|
return .date |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var validOperators: [ConditionOperator] { |
||||||
|
switch fieldType { |
||||||
|
case .string: return [.equals, .startsWith] |
||||||
|
case .int, .double, .date: return [.equals, .greaterThan, .lessThan] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var defaultValue: ConditionValue { |
||||||
|
switch fieldType { |
||||||
|
case .string: return .string("") |
||||||
|
case .int: return .int(0) |
||||||
|
case .double: return .double(0) |
||||||
|
case .date: return .date(Date()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
enum ConditionOperator: String, Codable, Identifiable, Sendable { |
||||||
|
case equals |
||||||
|
case startsWith |
||||||
|
case greaterThan |
||||||
|
case lessThan |
||||||
|
|
||||||
|
var id: String { rawValue } |
||||||
|
|
||||||
|
var displayName: String { |
||||||
|
switch self { |
||||||
|
case .equals: return "is" |
||||||
|
case .startsWith: return "starts with" |
||||||
|
case .greaterThan: return "is greater than" |
||||||
|
case .lessThan: return "is less than" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Tagged union storing the actual filter value with its type. |
||||||
|
// Uses custom Codable to survive JSON round-trips cleanly. |
||||||
|
enum ConditionValue: Equatable, Sendable { |
||||||
|
case string(String) |
||||||
|
case int(Int) |
||||||
|
case double(Double) |
||||||
|
case date(Date) |
||||||
|
|
||||||
|
var isEmpty: Bool { |
||||||
|
if case .string(let s) = self { |
||||||
|
return s.trimmingCharacters(in: .whitespaces).isEmpty |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension ConditionValue: Codable { |
||||||
|
private enum CodingKeys: String, CodingKey { case type, value } |
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
switch self { |
||||||
|
case .string(let s): |
||||||
|
try container.encode("string", forKey: .type) |
||||||
|
try container.encode(s, forKey: .value) |
||||||
|
case .int(let i): |
||||||
|
try container.encode("int", forKey: .type) |
||||||
|
try container.encode(i, forKey: .value) |
||||||
|
case .double(let d): |
||||||
|
try container.encode("double", forKey: .type) |
||||||
|
try container.encode(d, forKey: .value) |
||||||
|
case .date(let date): |
||||||
|
try container.encode("date", forKey: .type) |
||||||
|
try container.encode(date.timeIntervalSince1970, forKey: .value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
let type = try container.decode(String.self, forKey: .type) |
||||||
|
switch type { |
||||||
|
case "string": |
||||||
|
self = .string(try container.decode(String.self, forKey: .value)) |
||||||
|
case "int": |
||||||
|
self = .int(try container.decode(Int.self, forKey: .value)) |
||||||
|
case "double": |
||||||
|
self = .double(try container.decode(Double.self, forKey: .value)) |
||||||
|
case "date": |
||||||
|
self = .date(Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .value))) |
||||||
|
default: |
||||||
|
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown type: \(type)") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
nonisolated struct SmartPlaylistCondition: Codable, Equatable, Sendable { |
||||||
|
var field: TrackField |
||||||
|
var op: ConditionOperator |
||||||
|
var value: ConditionValue |
||||||
|
|
||||||
|
var isEmpty: Bool { value.isEmpty } |
||||||
|
} |
||||||
Loading…
Reference in new issue