Track.swift 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import Foundation
  2. import SwiftData
  3. /// Represents a single audio track in the library.
  4. @Model
  5. final class Track {
  6. var id: UUID
  7. var title: String
  8. var artist: String
  9. var album: String
  10. var genre: String
  11. /// Path relative to the app's documents directory (e.g. "Music/Artist - Title.mp3").
  12. var filePath: String
  13. /// Original filename without directory (used for cross-device matching).
  14. var fileName: String
  15. var duration: TimeInterval
  16. var bpm: Double?
  17. var musicalKey: String?
  18. var sampleRate: Double
  19. var bitDepth: Int
  20. var channels: Int
  21. var fileFormat: String
  22. var fileSizeBytes: Int64
  23. var year: Int? // release year from metadata
  24. var dateAdded: Date
  25. var lastPlayed: Date?
  26. var playCount: Int
  27. var rating: Int // 0-5 stars
  28. var color: String? // user-assigned color tag
  29. var notes: String
  30. // MARK: - Cloud (Chad Music) fields
  31. /// If true, this track is from Chad Music cloud — streamed via AVPlayer, not local file.
  32. var isCloud: Bool = false
  33. /// Chad Music server URL for streaming (e.g., "/music/Artist/Album/track.mp3").
  34. var cloudStreamPath: String?
  35. /// Chad Music track ID (hex string from server).
  36. var cloudTrackId: String?
  37. /// Cached waveform samples (downsampled min/max pairs), stored as Data for efficiency.
  38. var waveformData: Data?
  39. /// Whether BPM/key analysis has been performed.
  40. var isAnalyzed: Bool
  41. @Relationship(deleteRule: .cascade, inverse: \CuePoint.track)
  42. var cuePoints: [CuePoint]
  43. /// Resolve the full file URL from the app's documents directory.
  44. var fileURL: URL {
  45. let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
  46. return docs.appendingPathComponent(filePath).standardizedFileURL
  47. }
  48. /// True if this track has a valid local file (not a cloud-only track).
  49. var hasLocalFile: Bool {
  50. !filePath.isEmpty && !isCloud
  51. }
  52. var formattedDuration: String {
  53. let minutes = Int(duration) / 60
  54. let seconds = Int(duration) % 60
  55. return String(format: "%d:%02d", minutes, seconds)
  56. }
  57. var formattedBPM: String {
  58. guard let bpm else { return "—" }
  59. return String(format: "%.1f", bpm)
  60. }
  61. var formattedFileSize: String {
  62. ByteCountFormatter.string(fromByteCount: fileSizeBytes, countStyle: .file)
  63. }
  64. init(
  65. title: String,
  66. artist: String = "",
  67. album: String = "",
  68. genre: String = "",
  69. filePath: String,
  70. fileName: String = "",
  71. duration: TimeInterval = 0,
  72. sampleRate: Double = 44100,
  73. bitDepth: Int = 16,
  74. channels: Int = 2,
  75. fileFormat: String = "",
  76. fileSizeBytes: Int64 = 0
  77. ) {
  78. self.id = UUID()
  79. self.title = title
  80. self.artist = artist
  81. self.album = album
  82. self.genre = genre
  83. self.filePath = filePath
  84. self.fileName = fileName.isEmpty ? (filePath as NSString).lastPathComponent : fileName
  85. self.duration = duration
  86. self.bpm = nil
  87. self.musicalKey = nil
  88. self.sampleRate = sampleRate
  89. self.bitDepth = bitDepth
  90. self.channels = channels
  91. self.fileFormat = fileFormat
  92. self.fileSizeBytes = fileSizeBytes
  93. self.year = nil
  94. self.dateAdded = Date()
  95. self.lastPlayed = nil
  96. self.playCount = 0
  97. self.rating = 0
  98. self.color = nil
  99. self.notes = ""
  100. self.waveformData = nil
  101. self.isAnalyzed = false
  102. self.isCloud = false
  103. self.cloudStreamPath = nil
  104. self.cloudTrackId = nil
  105. self.cuePoints = []
  106. }
  107. /// Create a Track from a Chad Music cloud track.
  108. static func fromCloud(_ chadTrack: ChadTrack) -> Track {
  109. let track = Track(
  110. title: chadTrack.title,
  111. artist: chadTrack.artist ?? "",
  112. album: chadTrack.album ?? "",
  113. filePath: "",
  114. duration: chadTrack.duration ?? 0
  115. )
  116. track.isCloud = true
  117. track.cloudStreamPath = chadTrack.url
  118. track.cloudTrackId = chadTrack.id
  119. track.year = chadTrack.year
  120. return track
  121. }
  122. }