Playlist.swift 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import Foundation
  2. import SwiftData
  3. /// A playlist — an ordered collection of tracks for building a mix.
  4. @Model
  5. final class Playlist {
  6. var id: UUID = UUID()
  7. var name: String = ""
  8. var notes: String = ""
  9. var dateCreated: Date = Date()
  10. var dateModified: Date = Date()
  11. var targetBPM: Double? // target BPM for the mix
  12. var color: String = "#2196F3" // hex color for UI
  13. /// Template for grouping tracks in the playlist view.
  14. /// Uses {Artist}, {Album}, {Genre}, {Year}, {Folder} placeholders.
  15. /// Example: "{Album} ({Year})" → "Only Built 4 Cuban Linx (1995)"
  16. /// Empty string = no grouping.
  17. var groupTemplate: String = ""
  18. @Relationship(deleteRule: .cascade, inverse: \PlaylistEntry.playlist)
  19. var entries: [PlaylistEntry]
  20. /// The folder this playlist belongs to (nil = top level).
  21. var folder: PlaylistFolder?
  22. /// Entries sorted by position.
  23. var sortedEntries: [PlaylistEntry] {
  24. entries.sorted { $0.position < $1.position }
  25. }
  26. var totalDuration: TimeInterval {
  27. entries.compactMap(\.track?.duration).reduce(0, +)
  28. }
  29. var formattedTotalDuration: String {
  30. let total = Int(totalDuration)
  31. let hours = total / 3600
  32. let minutes = (total % 3600) / 60
  33. let seconds = total % 60
  34. if hours > 0 {
  35. return String(format: "%d:%02d:%02d", hours, minutes, seconds)
  36. }
  37. return String(format: "%d:%02d", minutes, seconds)
  38. }
  39. var trackCount: Int { entries.count }
  40. init(
  41. name: String,
  42. notes: String = "",
  43. color: String = "#2196F3"
  44. ) {
  45. self.id = UUID()
  46. self.name = name
  47. self.notes = notes
  48. self.dateCreated = Date()
  49. self.dateModified = Date()
  50. self.targetBPM = nil
  51. self.color = color
  52. self.groupTemplate = ""
  53. self.entries = []
  54. }
  55. func addTrack(_ track: Track, crossfadeDuration: TimeInterval = 0) {
  56. let position = entries.count
  57. let entry = PlaylistEntry(
  58. position: position,
  59. track: track,
  60. crossfadeDuration: crossfadeDuration
  61. )
  62. entries.append(entry)
  63. dateModified = Date()
  64. }
  65. func removeEntry(at position: Int) {
  66. entries.removeAll { $0.position == position }
  67. // Re-index positions
  68. for (index, entry) in sortedEntries.enumerated() {
  69. entry.position = index
  70. }
  71. dateModified = Date()
  72. }
  73. func moveEntry(from source: Int, to destination: Int) {
  74. var sorted = sortedEntries
  75. let entry = sorted.remove(at: source)
  76. sorted.insert(entry, at: destination)
  77. for (index, e) in sorted.enumerated() {
  78. e.position = index
  79. }
  80. dateModified = Date()
  81. }
  82. }
  83. /// A single entry in a playlist, linking a track with transition info.
  84. @Model
  85. final class PlaylistEntry {
  86. var id: UUID = UUID()
  87. var position: Int = 0
  88. var track: Track?
  89. // Transition settings
  90. var crossfadeDuration: TimeInterval = 0 // seconds of overlap with next track
  91. var startOffset: TimeInterval = 0 // start playback from this point
  92. var endOffset: TimeInterval = 0 // stop playback at this point (0 = end of track)
  93. var gainAdjustment: Double = 0 // dB adjustment (-12 to +12)
  94. var notes: String = ""
  95. var playlist: Playlist?
  96. /// Effective duration considering start/end offsets.
  97. var effectiveDuration: TimeInterval {
  98. guard let track else { return 0 }
  99. let end = endOffset > 0 ? endOffset : track.duration
  100. return max(0, end - startOffset)
  101. }
  102. init(
  103. position: Int,
  104. track: Track?,
  105. crossfadeDuration: TimeInterval = 0,
  106. startOffset: TimeInterval = 0,
  107. endOffset: TimeInterval = 0,
  108. gainAdjustment: Double = 0,
  109. notes: String = ""
  110. ) {
  111. self.id = UUID()
  112. self.position = position
  113. self.track = track
  114. self.crossfadeDuration = crossfadeDuration
  115. self.startOffset = startOffset
  116. self.endOffset = endOffset
  117. self.gainAdjustment = gainAdjustment
  118. self.notes = notes
  119. }
  120. }