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