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:
cdnutterandClaude Sonnet 5.5 committed 2026-10-24 17:08:51 -07:00
1 parent 894dd5a695
commit df5d847169
157 files changed
+3935 -742

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
import AVFoundation
import Foundation
import MediaPlayer
/// An internet radio stream the user saved in Browse > Web Radio.
struct WebStation: Codable, Hashable, Identifiable {
var id = UUID().uuidString
var name: String
var url: String
}
/// Plays a web radio stream through AVPlayer, outside the stem engine. Starting a station pauses
/// the queue, and playing the queue again stops the station. Main-thread only, like PlaybackManager.
@Observable
final class WebRadio: @unchecked Sendable {
private let pm: PlaybackManager
@ObservationIgnored private let player = AVPlayer()
@ObservationIgnored weak var nowPlaying: NowPlaying?
private(set) var station: WebStation?
private(set) var playing = false
/// A station is loaded (playing or paused); the lock screen belongs to the radio.
var active: Bool { station != nil }
init(pm: PlaybackManager) {
self.pm = pm
player.automaticallyWaitsToMinimizeStalling = true
pm.addListener { [weak self] in
guard let self, self.active, pm.playWhenReady else { return }
self.stop()
}
}
func play(_ s: WebStation) {
guard let url = URL(string: s.url) else { return }
station = s
if pm.playWhenReady { pm.pause() }
let a = AVAudioSession.sharedInstance()
try? a.setCategory(.playback, mode: .default, policy: .longFormAudio)
try? a.setActive(true)
player.replaceCurrentItem(with: AVPlayerItem(url: url))
player.play()
playing = true
publish()
}
func resume() {
guard active else { return }
// Live streams go stale while paused; rejoin at the live edge.
if let s = station { play(s) }
}
func pause() {
player.pause()
playing = false
publish()
}
func toggle() { if playing { pause() } else { resume() } }
func stop() {
player.pause()
player.replaceCurrentItem(with: nil)
station = nil
playing = false
nowPlaying?.refresh()
}
private func publish() {
guard let s = station else { return }
let c = MPNowPlayingInfoCenter.default()
c.nowPlayingInfo = [
MPMediaItemPropertyTitle: s.name,
MPMediaItemPropertyArtist: "Web Radio",
MPNowPlayingInfoPropertyIsLiveStream: true,
MPNowPlayingInfoPropertyPlaybackRate: playing ? 1.0 : 0.0,
]
c.playbackState = playing ? .playing : .paused
}
}