iOS: Random Album Radio plays whole albums in random order, loading each next album lazily
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a4c870251c
commit
3e832e3125
3 files changed
+45
-13
No files matched your search
@@ -11,3 +11,4 @@
|
|||||||
2026-10-28 iOS: stem playlists refresh their track metadata from Plex on each library load. Playlist covers can be changed from the playlist menu.
|
2026-10-28 iOS: stem playlists refresh their track metadata from Plex on each library load. Playlist covers can be changed from the playlist menu.
|
||||||
2026-10-28 iOS: Downloads 3-dots menu gets View Queue (waiting, downloading, failed, done).
|
2026-10-28 iOS: Downloads 3-dots menu gets View Queue (waiting, downloading, failed, done).
|
||||||
2026-10-28 iOS: Library screen can be reordered (Reorder button). Library Radio and Random Album Radio shuffle the whole library.
|
2026-10-28 iOS: Library screen can be reordered (Reorder button). Library Radio and Random Album Radio shuffle the whole library.
|
||||||
|
2026-10-28 iOS: Random Album Radio plays whole albums in track order, albums in random order across the library, loading the next album as one finishes.
|
||||||
@@ -201,7 +201,41 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
|||||||
messageListeners[id] = f
|
messageListeners[id] = f
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
private func changed() { listeners.values.forEach { $0() } }
|
private func changed() {
|
||||||
|
listeners.values.forEach { $0() }
|
||||||
|
topUpRadio()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Album radio: every album in the library in random order, each played whole and in track order.
|
||||||
|
/// Only the next album is fetched, once the queue is nearly played out.
|
||||||
|
@ObservationIgnored private var radioDeck: [Album]?
|
||||||
|
@ObservationIgnored private var radioBusy = false
|
||||||
|
|
||||||
|
func startAlbumRadio() async throws {
|
||||||
|
var deck = try await plex.albums().shuffled()
|
||||||
|
var first: [Track] = []
|
||||||
|
while first.isEmpty, !deck.isEmpty { first = (try? await plex.albumTracks(deck.removeLast().ratingKey)) ?? [] }
|
||||||
|
if first.isEmpty { return }
|
||||||
|
playTracks(first)
|
||||||
|
radioDeck = deck
|
||||||
|
}
|
||||||
|
|
||||||
|
private func topUpRadio() {
|
||||||
|
guard radioDeck != nil, !radioBusy, queue.count - index < 3 else { return }
|
||||||
|
radioBusy = true
|
||||||
|
Task { @MainActor in
|
||||||
|
defer { radioBusy = false }
|
||||||
|
while var deck = radioDeck, !deck.isEmpty {
|
||||||
|
let album = deck.removeLast()
|
||||||
|
radioDeck = deck
|
||||||
|
if let t = try? await plex.albumTracks(album.ratingKey), !t.isEmpty, radioDeck != nil {
|
||||||
|
addToQueue(t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if radioDeck != nil, let all = try? await plex.albums(), !all.isEmpty { radioDeck = all.shuffled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func positionMs() -> Int64 {
|
func positionMs() -> Int64 {
|
||||||
let snap = engine.snapshot
|
let snap = engine.snapshot
|
||||||
@@ -229,6 +263,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
|||||||
|
|
||||||
func playTracks(_ tracks: [Track], mixes: [[String: StemSetting]]? = nil, start: Int = 0, shuffle doShuffle: Bool = false) {
|
func playTracks(_ tracks: [Track], mixes: [[String: StemSetting]]? = nil, start: Int = 0, shuffle doShuffle: Bool = false) {
|
||||||
if tracks.isEmpty { return }
|
if tracks.isEmpty { return }
|
||||||
|
radioDeck = nil
|
||||||
let items = items(tracks, mixes)
|
let items = items(tracks, mixes)
|
||||||
if doShuffle {
|
if doShuffle {
|
||||||
unshuffled = items
|
unshuffled = items
|
||||||
|
|||||||
@@ -79,14 +79,12 @@ struct LibraryHome: View {
|
|||||||
}
|
}
|
||||||
if ready {
|
if ready {
|
||||||
SectionTitle(title: "Stations")
|
SectionTitle(title: "Stations")
|
||||||
let stations: [(String, String, () async throws -> [Track])] = [
|
let stations: [(String, String, () async throws -> Void)] = [
|
||||||
("dot.radiowaves.left.and.right", "Library Radio", { try await g.plex.allTracksShuffled() }),
|
("dot.radiowaves.left.and.right", "Library Radio", {
|
||||||
("square.stack", "Random Album Radio", {
|
let list = try await g.plex.allTracksShuffled()
|
||||||
// Whole albums in random order, across the entire library.
|
if !list.isEmpty { g.playback.playTracks(list) }
|
||||||
var out: [Track] = []
|
|
||||||
for a in try await g.plex.albums().shuffled() { out += try await g.plex.albumTracks(a.ratingKey) }
|
|
||||||
return out
|
|
||||||
}),
|
}),
|
||||||
|
("square.stack", "Random Album Radio", { try await g.playback.startAlbumRadio() }),
|
||||||
]
|
]
|
||||||
let shown = ordered(stations) { $0.1 }
|
let shown = ordered(stations) { $0.1 }
|
||||||
ForEach(shown, id: \.1) { st in
|
ForEach(shown, id: \.1) { st in
|
||||||
@@ -126,12 +124,10 @@ struct LibraryHome: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Starts a station: its songs play in the order given (albums stay whole).
|
/// Starts a station: its songs play in the order given (albums stay whole).
|
||||||
private func play(_ load: @escaping () async throws -> [Track]) {
|
private func play(_ start: @escaping () async throws -> Void) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do { try await start() }
|
||||||
let list = try await load()
|
catch { ui.overlays.hud("Couldn't start station", "exclamationmark.triangle") }
|
||||||
if list.isEmpty { ui.overlays.hud("Nothing to play", "music.note") } else { g.playback.playTracks(list) }
|
|
||||||
} catch { ui.overlays.hud("Couldn't start station", "exclamationmark.triangle") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user