Stemify iOS 1.1 and WebUI updates, repo restructure into desktop/ios/server/web
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
894dd5a695
commit
df5d847169
157 files changed
+3935
-742
No files matched your search
@@ -100,7 +100,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
"stem|\(entry.key)|\(file)|\(size)|\(q.rawValue)"
|
||||
}
|
||||
static func plexKey(_ track: Track) -> String {
|
||||
"plex|\(track.partId.map(String.init) ?? "null")|\(track.partSize.map(String.init) ?? "null")"
|
||||
"plex|\(track.ratingKey.firstIndex(of: "~").map { String(track.ratingKey[...$0]) } ?? "")\(track.partId.map(String.init) ?? "null")|\(track.partSize.map(String.init) ?? "null")"
|
||||
}
|
||||
|
||||
@ObservationIgnored private let settings: Settings
|
||||
@@ -131,6 +131,8 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
private(set) var songMixes: [String: [String: StemSetting]] = [:]
|
||||
/// Karaoke mode: vocals on, soft or out (per settings) on every song, lyrics on Now Playing.
|
||||
private(set) var karaoke = false
|
||||
/// The last mix set by hand; the next song starts from it (never a song's saved mix).
|
||||
@ObservationIgnored private var carryMix: [String: StemSetting] = [:]
|
||||
@ObservationIgnored private var preKaraokeMix: [String: StemSetting]?
|
||||
|
||||
@ObservationIgnored private var listeners: [UUID: () -> Void] = [:]
|
||||
@@ -142,9 +144,11 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
@ObservationIgnored private var loadTask: Task<Void, Never>?
|
||||
@ObservationIgnored private var nextTask: Task<Void, Never>?
|
||||
@ObservationIgnored private var currentToken: Int64 = -1
|
||||
@ObservationIgnored private var nextToken: Int64 = -1
|
||||
@ObservationIgnored private var nextIndex = -1
|
||||
@ObservationIgnored private var nextEntry: StemEntry?
|
||||
/// Sessions handed to the engine for a gapless switch, by token: which queue item and entry they are.
|
||||
/// Kept until a switch or load, since the engine may already have switched when the queue changes.
|
||||
@ObservationIgnored private var queued: [Int64: (uid: Int64, entry: StemEntry?)] = [:]
|
||||
/// The queue ran out and playback stopped at its last song.
|
||||
@ObservationIgnored private var finished = false
|
||||
@ObservationIgnored private var currentEntry: StemEntry?
|
||||
@ObservationIgnored private var pendingPositionMs: Int64 = 0
|
||||
@ObservationIgnored private var failures = 0
|
||||
@@ -218,13 +222,18 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
}
|
||||
|
||||
/// `mixes` lines up with `tracks`: each song plays with its own mix (stem playlists).
|
||||
/// Shuffles whole albums, keeping each album's tracks in order (Plexamp-style), when the setting is on.
|
||||
private func shuffled(_ items: [QueueItem]) -> [QueueItem] {
|
||||
items.shuffled()
|
||||
}
|
||||
|
||||
func playTracks(_ tracks: [Track], mixes: [[String: StemSetting]]? = nil, start: Int = 0, shuffle doShuffle: Bool = false) {
|
||||
if tracks.isEmpty { return }
|
||||
let items = items(tracks, mixes)
|
||||
if doShuffle {
|
||||
unshuffled = items
|
||||
let first = start > 0 && start < items.count ? items[start] : items.randomElement()!
|
||||
queue = [first] + items.filter { $0.uid != first.uid }.shuffled()
|
||||
queue = [first] + shuffled(items.filter { $0.uid != first.uid })
|
||||
shuffle = true
|
||||
load(0, 0, true)
|
||||
} else {
|
||||
@@ -245,8 +254,8 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
q.insert(contentsOf: items, at: min(max(at, 0), q.count))
|
||||
queue = q
|
||||
unshuffled = unshuffled.map { $0 + items }
|
||||
if index < 0 {
|
||||
load(0, 0, true)
|
||||
if index < 0 || finished {
|
||||
load(finished ? index + 1 : 0, 0, true)
|
||||
} else {
|
||||
requeueNext()
|
||||
changed()
|
||||
@@ -303,7 +312,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
let cur = q.indices.contains(idx) ? q[idx] : nil
|
||||
if !shuffle {
|
||||
unshuffled = q
|
||||
queue = Array(q.prefix(idx + 1)) + q.dropFirst(idx + 1).shuffled()
|
||||
queue = Array(q.prefix(idx + 1)) + shuffled(Array(q.dropFirst(idx + 1)))
|
||||
shuffle = true
|
||||
} else {
|
||||
let orig = unshuffled ?? q
|
||||
@@ -376,6 +385,14 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
else { pause(); seekTo(0) }
|
||||
}
|
||||
|
||||
/// Skips past the rest of the current album (holding Next).
|
||||
func nextAlbum() {
|
||||
guard let album = current?.track.albumKey, index >= 0 else { return next() }
|
||||
if let i = queue.indices.dropFirst(index + 1).first(where: { queue[$0].track.albumKey != album }) { load(i, 0, playWhenReady) }
|
||||
else if repeatMode != .Off { load(0, 0, playWhenReady) }
|
||||
else { pause(); seekTo(0) }
|
||||
}
|
||||
|
||||
func previous() {
|
||||
if positionMs() > 3000 || index <= 0 {
|
||||
seekTo(0)
|
||||
@@ -401,7 +418,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
engine.stop()
|
||||
reportTimeline("stopped")
|
||||
currentToken = -1
|
||||
nextToken = -1
|
||||
queued = [:]
|
||||
queue = []
|
||||
index = -1
|
||||
current = nil
|
||||
@@ -428,14 +445,17 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
/// Karaoke and friends: set every stem at once.
|
||||
func applyMix(_ m: [String: StemSetting]) {
|
||||
mix = m
|
||||
carryMix = m
|
||||
pushMix()
|
||||
}
|
||||
|
||||
/// The mix a song starts with: karaoke if that's on, else its stem playlist mix, else the song's saved mix,
|
||||
/// else cleared (or kept, per settings).
|
||||
private func startMix(_ item: QueueItem) -> [String: StemSetting] {
|
||||
if karaoke && !settings.keepStemMix { karaoke = false; preKaraokeMix = nil }
|
||||
if karaoke { return karaokeMix }
|
||||
return item.mix ?? songMixes[item.track.ratingKey] ?? (settings.keepStemMix ? mix : [:])
|
||||
if let l = MixPresets.locked(settings), let m = l.build(StemPlaylists.allStems) { return m }
|
||||
return item.mix ?? songMixes[item.track.ratingKey] ?? (settings.keepStemMix ? carryMix : [:])
|
||||
}
|
||||
|
||||
/// How much of the singer is left in karaoke mode. Soft is a guide under your own voice, as quiet as settings say.
|
||||
@@ -491,6 +511,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
var s = mix[name] ?? StemSetting()
|
||||
f(&s)
|
||||
mix[name] = s
|
||||
carryMix = mix
|
||||
pushMix()
|
||||
}
|
||||
|
||||
@@ -533,7 +554,8 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
let item = queue[i]
|
||||
loadTask?.cancel()
|
||||
nextTask?.cancel()
|
||||
nextToken = -1
|
||||
queued = [:]
|
||||
finished = false
|
||||
engine.queueNext(nil)
|
||||
index = i
|
||||
current = item
|
||||
@@ -649,7 +671,6 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
/// Builds the next track's session now so the switch can be gapless.
|
||||
private func prepareNext() {
|
||||
nextTask?.cancel()
|
||||
nextToken = -1
|
||||
engine.queueNext(nil)
|
||||
guard let i = upcomingIndex() else { return }
|
||||
let item = queue[i]
|
||||
@@ -666,10 +687,27 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
s.release()
|
||||
return
|
||||
}
|
||||
self.nextToken = s.token
|
||||
self.nextIndex = i
|
||||
self.nextEntry = entry
|
||||
self.queued[s.token] = (item.uid, entry)
|
||||
self.engine.queueNext(s)
|
||||
await self.precacheAhead(from: i)
|
||||
}
|
||||
}
|
||||
|
||||
/// Warms the cache for the songs after the gapless one, up to the Cache Ahead setting.
|
||||
private func precacheAhead(from i: Int) async {
|
||||
let n = settings.precacheCount - 1
|
||||
guard n > 0 else { return }
|
||||
for k in 1...n {
|
||||
let j = i + k
|
||||
guard !Task.isCancelled, j < queue.count else { return }
|
||||
let t = queue[j].track
|
||||
if playableOffline(t) { continue }
|
||||
guard let (s, _) = try? await openDetached(t, 0) else { continue }
|
||||
// Keep the session open until its prefetch has filled the cache.
|
||||
for _ in 0..<300 where !Task.isCancelled && !playableOffline(t) {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
}
|
||||
s.release()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,13 +747,14 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
}
|
||||
|
||||
func onTransition(_ token: Int64) {
|
||||
if token == currentToken || token != nextToken { return }
|
||||
guard token != currentToken, let q = queued[token] else { return }
|
||||
queued = [:]
|
||||
scrobble()
|
||||
currentToken = token
|
||||
nextToken = -1
|
||||
index = nextIndex
|
||||
current = queue.indices.contains(nextIndex) ? queue[nextIndex] : nil
|
||||
currentEntry = nextEntry
|
||||
let i = queue.firstIndex { $0.uid == q.uid } ?? min(index + 1, queue.count - 1)
|
||||
index = i
|
||||
current = queue.indices.contains(i) ? queue[i] : nil
|
||||
currentEntry = q.entry
|
||||
pendingPositionMs = 0
|
||||
scrobbled = -1
|
||||
guard let item = current else { return }
|
||||
@@ -743,6 +782,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
}
|
||||
playWhenReady = false
|
||||
pendingPositionMs = 0
|
||||
finished = true
|
||||
reportTimeline("stopped")
|
||||
changed()
|
||||
save()
|
||||
|
||||
Reference in new issue
Block a user