import Foundation import SwiftData /// A playlist — an ordered collection of tracks for building a mix. @Model final class Playlist { var id: UUID = UUID() var name: String = "" var notes: String = "" var dateCreated: Date = Date() var dateModified: Date = Date() var targetBPM: Double? // target BPM for the mix var color: String = "#2196F3" // hex color for UI /// Template for grouping tracks in the playlist view. /// Uses {Artist}, {Album}, {Genre}, {Year}, {Folder} placeholders. /// Example: "{Album} ({Year})" → "Only Built 4 Cuban Linx (1995)" /// Empty string = no grouping. var groupTemplate: String = "" @Relationship(deleteRule: .cascade, inverse: \PlaylistEntry.playlist) var entries: [PlaylistEntry] /// The folder this playlist belongs to (nil = top level). var folder: PlaylistFolder? /// Entries sorted by position. var sortedEntries: [PlaylistEntry] { entries.sorted { $0.position < $1.position } } var totalDuration: TimeInterval { entries.compactMap(\.track?.duration).reduce(0, +) } var formattedTotalDuration: String { let total = Int(totalDuration) let hours = total / 3600 let minutes = (total % 3600) / 60 let seconds = total % 60 if hours > 0 { return String(format: "%d:%02d:%02d", hours, minutes, seconds) } return String(format: "%d:%02d", minutes, seconds) } var trackCount: Int { entries.count } init( name: String, notes: String = "", color: String = "#2196F3" ) { self.id = UUID() self.name = name self.notes = notes self.dateCreated = Date() self.dateModified = Date() self.targetBPM = nil self.color = color self.groupTemplate = "" self.entries = [] } func addTrack(_ track: Track, crossfadeDuration: TimeInterval = 0) { let position = entries.count let entry = PlaylistEntry( position: position, track: track, crossfadeDuration: crossfadeDuration ) entries.append(entry) dateModified = Date() } func removeEntry(at position: Int) { entries.removeAll { $0.position == position } // Re-index positions for (index, entry) in sortedEntries.enumerated() { entry.position = index } dateModified = Date() } func moveEntry(from source: Int, to destination: Int) { var sorted = sortedEntries let entry = sorted.remove(at: source) sorted.insert(entry, at: destination) for (index, e) in sorted.enumerated() { e.position = index } dateModified = Date() } } /// A single entry in a playlist, linking a track with transition info. @Model final class PlaylistEntry { var id: UUID = UUID() var position: Int = 0 var track: Track? // Transition settings var crossfadeDuration: TimeInterval = 0 // seconds of overlap with next track var startOffset: TimeInterval = 0 // start playback from this point var endOffset: TimeInterval = 0 // stop playback at this point (0 = end of track) var gainAdjustment: Double = 0 // dB adjustment (-12 to +12) var notes: String = "" var playlist: Playlist? /// Effective duration considering start/end offsets. var effectiveDuration: TimeInterval { guard let track else { return 0 } let end = endOffset > 0 ? endOffset : track.duration return max(0, end - startOffset) } init( position: Int, track: Track?, crossfadeDuration: TimeInterval = 0, startOffset: TimeInterval = 0, endOffset: TimeInterval = 0, gainAdjustment: Double = 0, notes: String = "" ) { self.id = UUID() self.position = position self.track = track self.crossfadeDuration = crossfadeDuration self.startOffset = startOffset self.endOffset = endOffset self.gainAdjustment = gainAdjustment self.notes = notes } }