iOS: stem playlist refresh and covers, download queue page, reorderable Library, whole-library radio
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c8dfb54ca
commit
a4c870251c
18 files changed
+514
-30
No files matched your search
@@ -19,6 +19,23 @@ private struct ServerInfo: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
/// What stemd is doing, from stemserve's /api/queue.
|
||||
enum StemState: Equatable { case stemming, queued, failed }
|
||||
|
||||
private struct QueueInfo: Decodable {
|
||||
var pending: [String] = []
|
||||
var current: String?
|
||||
var done = 0
|
||||
var failed: [String] = []
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: AnyKey.self)
|
||||
pending = c.list("pending")
|
||||
current = c.str("current")
|
||||
done = c.int("done") ?? 0
|
||||
failed = c.list("failed")
|
||||
}
|
||||
}
|
||||
|
||||
/// Client for server/stemserve.py: the stems index, stem file URLs and waveforms.
|
||||
@Observable
|
||||
final class StemServer: @unchecked Sendable {
|
||||
@@ -35,6 +52,13 @@ final class StemServer: @unchecked Sendable {
|
||||
private(set) var status = StemServerStatus.unknown
|
||||
/// When the index was last checked against the server, nil before the first check.
|
||||
private(set) var checkedAt: Date?
|
||||
/// stemd's live state: the song being separated, the ones waiting, the ones that failed.
|
||||
private(set) var current: String?
|
||||
private(set) var pending: Set<String> = []
|
||||
private(set) var failed: Set<String> = []
|
||||
@ObservationIgnored private var lastDone: Int?
|
||||
|
||||
var busy: Bool { current != nil || !pending.isEmpty }
|
||||
|
||||
init(http: Http, settings: Settings, dir: URL) {
|
||||
self.http = http
|
||||
@@ -53,6 +77,49 @@ final class StemServer: @unchecked Sendable {
|
||||
try? await Task.sleep(nanoseconds: Self.checkSeconds * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
// Watch stemd's queue: every few seconds while it has work, slowly when idle.
|
||||
Task.detached { [weak self] in
|
||||
while let self {
|
||||
await self.pollQueue()
|
||||
try? await Task.sleep(nanoseconds: (self.busy ? 4 : 30) * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a track stands with stemd, nil when it has stems or isn't known to it.
|
||||
func state(_ track: Track) -> StemState? {
|
||||
guard let f = track.partFile, !f.isEmpty else { return nil }
|
||||
if find(track) != nil { return nil }
|
||||
if let c = current, f == c || f.hasSuffix("/" + c) { return .stemming }
|
||||
if pending.contains(f) { return .queued }
|
||||
if failed.contains(f) { return .failed }
|
||||
return nil
|
||||
}
|
||||
|
||||
/// "Stemming “X” · N queued", nil when stemd is idle.
|
||||
var liveLine: String? {
|
||||
let n = pending.count
|
||||
if let c = current {
|
||||
let name = ((c as NSString).lastPathComponent as NSString).deletingPathExtension
|
||||
return "Stemming “\(name)”" + (n > 0 ? " · \(n) queued" : "")
|
||||
}
|
||||
return n > 0 ? "\(n) queued for stems" : nil
|
||||
}
|
||||
|
||||
/// Reads /api/queue. A change in the finished count pulls the new index straight away.
|
||||
func pollQueue() async {
|
||||
guard !base.isEmpty,
|
||||
let u = try? url("/api/queue"),
|
||||
let data = try? await http.data(req(u)),
|
||||
let q = try? JSONDecoder().decode(QueueInfo.self, from: data) else { return }
|
||||
let changed = lastDone != nil && lastDone != q.done
|
||||
lastDone = q.done
|
||||
await MainActor.run {
|
||||
self.current = q.current
|
||||
self.pending = Set(q.pending)
|
||||
self.failed = Set(q.failed)
|
||||
}
|
||||
if changed { await refresh() }
|
||||
}
|
||||
|
||||
private var base: String { settings.stemUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/")) }
|
||||
@@ -164,6 +231,8 @@ final class StemServer: @unchecked Sendable {
|
||||
let msg = (try? JSONSerialization.jsonObject(with: body) as? [String: Any])?["error"] as? String
|
||||
throw HttpError(code: code, message: code == 404 ? "stemserve needs updating for Stem Now" : msg ?? "stemserve answered \(code)")
|
||||
}
|
||||
await MainActor.run { paths.forEach { self.pending.insert($0) } }
|
||||
Task.detached { [self] in await pollQueue() }
|
||||
return paths.count
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user