velody/packages/apple/VelodyPlayback/Sources/VelodyPlayback/PlaybackQueue.swift
2026-05-28 14:05:15 +02:00

230 lines
6.7 KiB
Swift

import Foundation
public struct PlaybackQueue: Hashable, Sendable {
public private(set) var catalogTrackIDs: [String]
public private(set) var queuedTrackIDs: [String]
public private(set) var currentTrackID: String?
public private(set) var isShuffleEnabled: Bool
public private(set) var repeatMode: PlaybackRepeatMode
public init(
trackIDs: [String] = [],
currentTrackID: String? = nil,
queuedTrackIDs: [String]? = nil,
isShuffleEnabled: Bool = false,
repeatMode: PlaybackRepeatMode = .off
) {
self.catalogTrackIDs = []
self.queuedTrackIDs = []
self.currentTrackID = nil
self.isShuffleEnabled = isShuffleEnabled
self.repeatMode = repeatMode
replaceTrackIDs(
trackIDs,
currentTrackID: currentTrackID,
queuedTrackIDs: queuedTrackIDs
)
}
public var isEmpty: Bool {
queuedTrackIDs.isEmpty
}
public mutating func replaceTrackIDs(
_ trackIDs: [String],
currentTrackID preferredCurrentTrackID: String? = nil,
queuedTrackIDs preferredQueuedTrackIDs: [String]? = nil
) {
let normalizedCatalogTrackIDs = Self.uniqueTrackIDs(trackIDs)
catalogTrackIDs = normalizedCatalogTrackIDs
if normalizedCatalogTrackIDs.isEmpty {
queuedTrackIDs = []
currentTrackID = nil
return
}
if isShuffleEnabled {
if let preferredQueuedTrackIDs {
let normalizedQueuedTrackIDs = Self.normalizedQueuedTrackIDs(
preferredQueuedTrackIDs,
validTrackIDs: normalizedCatalogTrackIDs
)
queuedTrackIDs = normalizedQueuedTrackIDs.isEmpty
? Self.makeShuffledTrackIDs(
from: normalizedCatalogTrackIDs,
currentTrackID: preferredCurrentTrackID ?? currentTrackID
)
: normalizedQueuedTrackIDs
} else {
let normalizedCurrentQueue = Self.normalizedQueuedTrackIDs(
queuedTrackIDs,
validTrackIDs: normalizedCatalogTrackIDs
)
queuedTrackIDs = normalizedCurrentQueue.isEmpty
? Self.makeShuffledTrackIDs(
from: normalizedCatalogTrackIDs,
currentTrackID: preferredCurrentTrackID ?? currentTrackID
)
: normalizedCurrentQueue
}
} else {
queuedTrackIDs = normalizedCatalogTrackIDs
}
if let preferredCurrentTrackID,
queuedTrackIDs.contains(preferredCurrentTrackID)
{
currentTrackID = preferredCurrentTrackID
} else if let currentTrackID,
queuedTrackIDs.contains(currentTrackID)
{
self.currentTrackID = currentTrackID
} else {
currentTrackID = nil
}
}
public mutating func selectTrack(_ trackID: String) {
guard queuedTrackIDs.contains(trackID) else {
return
}
currentTrackID = trackID
}
public mutating func toggleShuffle() {
setShuffleEnabled(!isShuffleEnabled)
}
public mutating func setShuffleEnabled(
_ isEnabled: Bool,
queuedTrackIDs preferredQueuedTrackIDs: [String]? = nil
) {
isShuffleEnabled = isEnabled
replaceTrackIDs(
catalogTrackIDs,
currentTrackID: currentTrackID,
queuedTrackIDs: preferredQueuedTrackIDs
)
}
public mutating func cycleRepeatMode() {
repeatMode = repeatMode.nextMode
}
public mutating func setRepeatMode(_ repeatMode: PlaybackRepeatMode) {
self.repeatMode = repeatMode
}
public func nextTrackID() -> String? {
guard let currentTrackID else {
return queuedTrackIDs.first
}
guard let currentIndex = queuedTrackIDs.firstIndex(of: currentTrackID) else {
return queuedTrackIDs.first
}
if repeatMode == .one {
return currentTrackID
}
let nextIndex = currentIndex + 1
if queuedTrackIDs.indices.contains(nextIndex) {
return queuedTrackIDs[nextIndex]
}
if repeatMode == .all {
return queuedTrackIDs.first
}
return nil
}
public func previousTrackID() -> String? {
guard let currentTrackID else {
return queuedTrackIDs.first
}
guard let currentIndex = queuedTrackIDs.firstIndex(of: currentTrackID) else {
return queuedTrackIDs.first
}
if repeatMode == .one {
return currentTrackID
}
let previousIndex = currentIndex - 1
if queuedTrackIDs.indices.contains(previousIndex) {
return queuedTrackIDs[previousIndex]
}
if repeatMode == .all {
return queuedTrackIDs.last
}
return nil
}
public mutating func advanceToNextTrack() -> String? {
let nextTrackID = nextTrackID()
if let nextTrackID {
currentTrackID = nextTrackID
}
return nextTrackID
}
public mutating func moveToPreviousTrack() -> String? {
let previousTrackID = previousTrackID()
if let previousTrackID {
currentTrackID = previousTrackID
}
return previousTrackID
}
private static func uniqueTrackIDs(_ trackIDs: [String]) -> [String] {
var seenTrackIDs = Set<String>()
return trackIDs.filter { trackID in
seenTrackIDs.insert(trackID).inserted
}
}
private static func normalizedQueuedTrackIDs(
_ queuedTrackIDs: [String],
validTrackIDs: [String]
) -> [String] {
let validTrackIDSet = Set(validTrackIDs)
var seenTrackIDs = Set<String>()
let normalizedQueuedTrackIDs = queuedTrackIDs.filter { trackID in
validTrackIDSet.contains(trackID) && seenTrackIDs.insert(trackID).inserted
}
let missingTrackIDs = validTrackIDs.filter { !seenTrackIDs.contains($0) }
return normalizedQueuedTrackIDs + missingTrackIDs
}
private static func makeShuffledTrackIDs(
from trackIDs: [String],
currentTrackID: String?
) -> [String] {
guard !trackIDs.isEmpty else {
return []
}
let remainingTrackIDs = trackIDs.filter { $0 != currentTrackID }.shuffled()
if let currentTrackID,
trackIDs.contains(currentTrackID)
{
return [currentTrackID] + remainingTrackIDs
}
return trackIDs.shuffled()
}
}