import Combine import SwiftUI @MainActor final class NowPlayingController: ObservableObject { @Published var currentTrack: MopidyPlaybackState.Track? @Published var isPlaying = false @Published var volume = 70 @Published var coverArt: UIImage? @Published var connectionStatus: String = "Connecting..." private let settings = AppSettings.shared private let streamPlayer = StreamPlayer.shared private var cancellables = Set() private var refreshTimer: Timer? init() { bindStreamPlayer() startPolling() fetchVolume() } func connect() { streamPlayer.playStream(host: settings.host, password: settings.password) } func reconnect() { streamPlayer.reconnect(host: settings.host, password: settings.password) } func togglePlay() { if isPlaying { streamPlayer.pause() Task { try? await MopidyClient.shared.pause() } } else { streamPlayer.resume() Task { try? await MopidyClient.shared.play() } } } func setVolume(_ newVolume: Int) { volume = newVolume Task { [weak self] in try? await MopidyClient.shared.setVolume(newVolume) } } // MARK: - Private private func bindStreamPlayer() { streamPlayer.$connectionStatus .map { status in switch status { case .connected: return "Connected" case .disconnected: return "Disconnected" case .error(let msg): return "Error: \(msg)" } } .assign(to: &$connectionStatus) streamPlayer.$isPlaying .assign(to: &$isPlaying) } private func startPolling() { refreshTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in Task { [weak self] in guard let self else { return } do { let state = try await MopidyClient.shared.getPlaybackState() if let track = state.tlTrack?.track { self.currentTrack = track self.fetchCoverArt(track.coverArtURI) } self.isPlaying = (state.state == .playing) } catch { self.connectionStatus = "Poll error" } } } } private func fetchCoverArt(_ uris: [String]?) { guard let firstURI = uris?.first, let url = URL(string: firstURI) else { return } Task { do { let (data, _) = try await URLSession.shared.data(from: url) if let image = UIImage(data: data) { self.coverArt = image } } catch { self.coverArt = nil } } } private func fetchVolume() { Task { do { self.volume = try await MopidyClient.shared.getVolume() } catch { print("Volume fetch error: \(error)") } } } }