| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- import Foundation
- import SwiftData
- /// Represents a single audio track in the library.
- @Model
- final class Track {
- var id: UUID
- var title: String
- var artist: String
- var album: String
- var genre: String
- /// Path relative to the app's documents directory (e.g. "Music/Artist - Title.mp3").
- var filePath: String
- /// Original filename without directory (used for cross-device matching).
- var fileName: String
- var duration: TimeInterval
- var bpm: Double?
- var musicalKey: String?
- var sampleRate: Double
- var bitDepth: Int
- var channels: Int
- var fileFormat: String
- var fileSizeBytes: Int64
- var year: Int? // release year from metadata
- var dateAdded: Date
- var lastPlayed: Date?
- var playCount: Int
- var rating: Int // 0-5 stars
- var color: String? // user-assigned color tag
- 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?
- /// 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
- @Relationship(deleteRule: .cascade, inverse: \CuePoint.track)
- var cuePoints: [CuePoint]
- /// Resolve the full file URL from the app's documents directory.
- var fileURL: URL {
- let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
- return docs.appendingPathComponent(filePath).standardizedFileURL
- }
- /// True if this track has a valid local file (not a cloud-only track).
- var hasLocalFile: Bool {
- !filePath.isEmpty && !isCloud
- }
- 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,
- fileName: 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.fileName = fileName.isEmpty ? (filePath as NSString).lastPathComponent : fileName
- 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.year = nil
- self.dateAdded = Date()
- self.lastPlayed = nil
- self.playCount = 0
- self.rating = 0
- self.color = nil
- self.notes = ""
- self.waveformData = nil
- self.isAnalyzed = false
- self.isCloud = false
- self.cloudStreamPath = nil
- self.cloudTrackId = nil
- 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
- }
- }
|