| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- 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
- }
- }
|