| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- 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
- }
- }
|