Track.swift 3.7 KB

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