From 4021881f116a2d86f5c16d84c5ed0566d6a35289 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sun, 5 Oct 2025 18:19:51 -0700 Subject: [PATCH] Resolve various connection issues --- QueueCube/Backend/API.swift | 23 ++++++++++++------- QueueCube/Views/ContentView.swift | 21 +++++++++++++---- QueueCube/Views/MainView.swift | 10 +++++--- QueueCube/Views/NowPlayingMiniView.swift | 10 +++++--- .../Views/Settings View/AddServerView.swift | 5 +++- 5 files changed, 49 insertions(+), 20 deletions(-) diff --git a/QueueCube/Backend/API.swift b/QueueCube/Backend/API.swift index bb9822f..5e42b21 100644 --- a/QueueCube/Backend/API.swift +++ b/QueueCube/Backend/API.swift @@ -54,10 +54,12 @@ struct NowPlayingInfo: Codable let volume: Int } -struct API +actor API { let baseURL: URL + private var pingTask: Task<(), any Swift.Error>? = nil + init(baseURL: URL) { self.baseURL = baseURL } @@ -168,17 +170,20 @@ struct API } public func events() async throws -> AsyncStream { + let requestBuilder: () -> RequestBuilder = request + return AsyncStream { continuation in - var websocketTask: URLSessionWebSocketTask = spawnWebsocketTask(with: continuation) + let websocketTask: URLSessionWebSocketTask = API.spawnWebsocketTask(requestBuilder: requestBuilder, with: continuation) Task { - while true { + var pingLoopEnabled = true + while pingLoopEnabled { try await Task.sleep(for: .seconds(5)) websocketTask.sendPing { error in if let error { - notifyError(error, continuation: continuation) - websocketTask = spawnWebsocketTask(with: continuation) + API.notifyError(error, continuation: continuation) + pingLoopEnabled = false } else { continuation.yield(.event(Event(type: .receivedWebsocketPong))) } @@ -188,11 +193,12 @@ struct API } } - private func spawnWebsocketTask( + private static func spawnWebsocketTask( + requestBuilder: () -> RequestBuilder, with continuation: AsyncStream.Continuation ) -> URLSessionWebSocketTask { - let url = request() + let url = requestBuilder() .path("/events") .websocket() @@ -225,8 +231,9 @@ struct API return websocketTask } - private func notifyError(_ error: any Swift.Error, continuation: AsyncStream.Continuation) { + private static func notifyError(_ error: any Swift.Error, continuation: AsyncStream.Continuation) { print("Websocket Error: \(error)") + var shouldNotifyObservers = true let nsError = error as NSError if nsError.code == 53 { diff --git a/QueueCube/Views/ContentView.swift b/QueueCube/Views/ContentView.swift index 08529cd..218cace 100644 --- a/QueueCube/Views/ContentView.swift +++ b/QueueCube/Views/ContentView.swift @@ -98,10 +98,17 @@ extension ContentView for await streamEvent in try await api.events() { switch streamEvent { case .event(let event): - model.connectionError = nil + await clearConnectionErrorIfNecessary() await handle(event: event) case .error(let error): model.connectionError = error + + Task { @MainActor in + try await Task.sleep(for: .seconds(1.0)) + websocketRestartTrigger += 1 + } + + break } } } catch { @@ -128,10 +135,14 @@ extension ContentView case .receivedWebsocketPong: // This means we're online. - if model.connectionError != nil { - model.connectionError = nil - await refresh([.playlist, .nowPlaying, .favorites]) - } + await clearConnectionErrorIfNecessary() + } + } + + private func clearConnectionErrorIfNecessary() async { + if model.connectionError != nil { + model.connectionError = nil + await refresh([.playlist, .nowPlaying, .favorites]) } } diff --git a/QueueCube/Views/MainView.swift b/QueueCube/Views/MainView.swift index eb501b6..2116278 100644 --- a/QueueCube/Views/MainView.swift +++ b/QueueCube/Views/MainView.swift @@ -149,10 +149,14 @@ class MainViewModel withObservationTracking { _ = nowPlayingViewModel.volume } onChange: { [weak self] in + guard let self else { return } + + let isRefreshing = isRefreshingFromAPI Task { - guard let self else { return } - await self.withModificationsViaAPI { api in - try await api.setVolume(self.nowPlayingViewModel.volume) + if !isRefreshing { + await self.withModificationsViaAPI { api in + try await api.setVolume(self.nowPlayingViewModel.volume) + } } await MainActor.run { self.observeNowPlayingModel() } diff --git a/QueueCube/Views/NowPlayingMiniView.swift b/QueueCube/Views/NowPlayingMiniView.swift index e2c0715..bcb57c1 100644 --- a/QueueCube/Views/NowPlayingMiniView.swift +++ b/QueueCube/Views/NowPlayingMiniView.swift @@ -12,7 +12,11 @@ struct NowPlayingMiniView: View { let onTap: () -> Void @GestureState private var tapGestureState = false - private var nothingQueued: Bool { model.title == nil && model.subtitle == nil } + + private var nothingQueued: Bool { + guard let title = model.title, let subtitle = model.subtitle else { return true } + return title.isEmpty && subtitle.isEmpty + } var body: some View { let playPauseImageName = model.isPlaying ? "pause.fill" : "play.fill" @@ -26,14 +30,14 @@ struct NowPlayingMiniView: View { HStack { VStack(alignment: .leading) { - if let title = model.title { + if let title = model.title, !title.isEmpty { Text(title) .font(.caption) .lineLimit(1) .bold() } - if let subtitle = model.subtitle { + if let subtitle = model.subtitle, !subtitle.isEmpty { Text(subtitle) .lineLimit(1) .font(.caption) diff --git a/QueueCube/Views/Settings View/AddServerView.swift b/QueueCube/Views/Settings View/AddServerView.swift index 498b52d..bc861cf 100644 --- a/QueueCube/Views/Settings View/AddServerView.swift +++ b/QueueCube/Views/Settings View/AddServerView.swift @@ -175,7 +175,10 @@ struct AddServerView: View _ = try await api.fetchNowPlayingInfo() self.validationState = .valid - self.serverURL = self.validationURL + + if validationURL != serverURL { + self.serverURL = self.validationURL + } } catch { print("Validation failed: \(error)")