import Foundation import SwiftData /// Download state for cloud tracks. enum DownloadState: String, Codable { case none case downloading case downloaded case error } /// Represents a single audio track in the library. @Model final class Track { var id: UUID = UUID() var title: String = "" var artist: String = "" var album: String = "" var genre: String = "" var filePath: String = "" var duration: TimeInterval = 0 var bpm: Double? var musicalKey: String? var sampleRate: Double = 44100 var bitDepth: Int = 16 var channels: Int = 2 var fileFormat: String = "" var fileSizeBytes: Int64 = 0 var dateAdded: Date = Date() var lastPlayed: Date? var playCount: Int = 0 var rating: Int = 0 // 0-5 stars var color: String? // user-assigned color tag var year: Int? // release year from metadata var notes: String = "" // MARK: - Cloud (Chad Music) fields /// If true, this track is from Chad Music cloud — streamed via AVPlayer, not local file. var isCloud: Bool = false /// Chad Music server URL for streaming (e.g., "/music/Artist/Album/track.mp3"). var cloudStreamPath: String? /// Chad Music track ID (hex string from server). var cloudTrackId: String? /// Local cache path for downloaded cloud tracks (persistent offline storage). var localCachePath: String? /// Download state for cloud tracks — stored as raw string value of DownloadState enum. var downloadStateRaw: String = DownloadState.none.rawValue /// Cached waveform samples (downsampled min/max pairs), stored as Data for efficiency. var waveformData: Data? /// Whether BPM/key analysis has been performed. var isAnalyzed: Bool = false @Relationship(deleteRule: .cascade, inverse: \CuePoint.track) var cuePoints: [CuePoint] var fileURL: URL { URL(fileURLWithPath: filePath.isEmpty ? "/dev/null" : filePath) } /// True if this track has a valid local file (not a cloud-only track). var hasLocalFile: Bool { !filePath.isEmpty && !isCloud } /// Download state as typed enum (read/write through downloadStateRaw). var downloadState: DownloadState { get { DownloadState(rawValue: downloadStateRaw) ?? .none } set { downloadStateRaw = newValue.rawValue } } /// True if this track has a playable local file — either a local library track or a downloaded cloud track. /// Performs stale file recovery: if localCachePath is set but file is missing, resets download state. var hasPlayableLocalFile: Bool { // Local library track if !filePath.isEmpty && FileManager.default.fileExists(atPath: filePath) { return true } // Downloaded cloud track if let cachePath = localCachePath { if FileManager.default.fileExists(atPath: cachePath) { return true } // Stale file recovery — file missing, reset state localCachePath = nil downloadState = .none } return false } /// URL for the best available local file (prefers filePath, falls back to localCachePath). var playableFileURL: URL { if !filePath.isEmpty && FileManager.default.fileExists(atPath: filePath) { return URL(fileURLWithPath: filePath) } if let cachePath = localCachePath, FileManager.default.fileExists(atPath: cachePath) { return URL(fileURLWithPath: cachePath) } return URL(fileURLWithPath: "/dev/null") } var formattedDuration: String { let minutes = Int(duration) / 60 let seconds = Int(duration) % 60 return String(format: "%d:%02d", minutes, seconds) } var formattedBPM: String { guard let bpm else { return "—" } return String(format: "%.1f", bpm) } var formattedFileSize: String { ByteCountFormatter.string(fromByteCount: fileSizeBytes, countStyle: .file) } init( title: String, artist: String = "", album: String = "", genre: String = "", filePath: String, duration: TimeInterval = 0, sampleRate: Double = 44100, bitDepth: Int = 16, channels: Int = 2, fileFormat: String = "", fileSizeBytes: Int64 = 0 ) { self.id = UUID() self.title = title self.artist = artist self.album = album self.genre = genre self.filePath = filePath self.duration = duration self.bpm = nil self.musicalKey = nil self.sampleRate = sampleRate self.bitDepth = bitDepth self.channels = channels self.fileFormat = fileFormat self.fileSizeBytes = fileSizeBytes self.dateAdded = Date() self.lastPlayed = nil self.playCount = 0 self.rating = 0 self.color = nil self.year = nil self.notes = "" self.waveformData = nil self.isAnalyzed = false self.cuePoints = [] } /// Create a Track from a Chad Music cloud track. static func fromCloud(_ chadTrack: ChadTrack) -> Track { let track = Track( title: chadTrack.title, artist: chadTrack.artist ?? "", album: chadTrack.album ?? "", filePath: "", duration: chadTrack.duration ?? 0 ) track.isCloud = true track.cloudStreamPath = chadTrack.url track.cloudTrackId = chadTrack.id track.year = chadTrack.year return track } }