230 lines
6.7 KiB
Swift
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()
|
|
}
|
|
}
|