Resolve various connection issues

This commit is contained in:
2025-10-05 18:19:51 -07:00
parent 839ec53c17
commit 4021881f11
5 changed files with 49 additions and 20 deletions

View File

@@ -54,10 +54,12 @@ struct NowPlayingInfo: Codable
let volume: Int let volume: Int
} }
struct API actor API
{ {
let baseURL: URL let baseURL: URL
private var pingTask: Task<(), any Swift.Error>? = nil
init(baseURL: URL) { init(baseURL: URL) {
self.baseURL = baseURL self.baseURL = baseURL
} }
@@ -168,17 +170,20 @@ struct API
} }
public func events() async throws -> AsyncStream<StreamEvent> { public func events() async throws -> AsyncStream<StreamEvent> {
let requestBuilder: () -> RequestBuilder = request
return AsyncStream { continuation in return AsyncStream { continuation in
var websocketTask: URLSessionWebSocketTask = spawnWebsocketTask(with: continuation) let websocketTask: URLSessionWebSocketTask = API.spawnWebsocketTask(requestBuilder: requestBuilder, with: continuation)
Task { Task {
while true { var pingLoopEnabled = true
while pingLoopEnabled {
try await Task.sleep(for: .seconds(5)) try await Task.sleep(for: .seconds(5))
websocketTask.sendPing { error in websocketTask.sendPing { error in
if let error { if let error {
notifyError(error, continuation: continuation) API.notifyError(error, continuation: continuation)
websocketTask = spawnWebsocketTask(with: continuation) pingLoopEnabled = false
} else { } else {
continuation.yield(.event(Event(type: .receivedWebsocketPong))) continuation.yield(.event(Event(type: .receivedWebsocketPong)))
} }
@@ -188,11 +193,12 @@ struct API
} }
} }
private func spawnWebsocketTask( private static func spawnWebsocketTask(
requestBuilder: () -> RequestBuilder,
with continuation: AsyncStream<StreamEvent>.Continuation with continuation: AsyncStream<StreamEvent>.Continuation
) -> URLSessionWebSocketTask ) -> URLSessionWebSocketTask
{ {
let url = request() let url = requestBuilder()
.path("/events") .path("/events")
.websocket() .websocket()
@@ -225,8 +231,9 @@ struct API
return websocketTask return websocketTask
} }
private func notifyError(_ error: any Swift.Error, continuation: AsyncStream<StreamEvent>.Continuation) { private static func notifyError(_ error: any Swift.Error, continuation: AsyncStream<StreamEvent>.Continuation) {
print("Websocket Error: \(error)") print("Websocket Error: \(error)")
var shouldNotifyObservers = true var shouldNotifyObservers = true
let nsError = error as NSError let nsError = error as NSError
if nsError.code == 53 { if nsError.code == 53 {

View File

@@ -98,10 +98,17 @@ extension ContentView
for await streamEvent in try await api.events() { for await streamEvent in try await api.events() {
switch streamEvent { switch streamEvent {
case .event(let event): case .event(let event):
model.connectionError = nil await clearConnectionErrorIfNecessary()
await handle(event: event) await handle(event: event)
case .error(let error): case .error(let error):
model.connectionError = error model.connectionError = error
Task { @MainActor in
try await Task.sleep(for: .seconds(1.0))
websocketRestartTrigger += 1
}
break
} }
} }
} catch { } catch {
@@ -128,12 +135,16 @@ extension ContentView
case .receivedWebsocketPong: case .receivedWebsocketPong:
// This means we're online. // This means we're online.
await clearConnectionErrorIfNecessary()
}
}
private func clearConnectionErrorIfNecessary() async {
if model.connectionError != nil { if model.connectionError != nil {
model.connectionError = nil model.connectionError = nil
await refresh([.playlist, .nowPlaying, .favorites]) await refresh([.playlist, .nowPlaying, .favorites])
} }
} }
}
private func watchForSettingsChanges() async { private func watchForSettingsChanges() async {
let settingsChangedNotifications = NotificationCenter.default.notifications(named: .settingsChanged) let settingsChangedNotifications = NotificationCenter.default.notifications(named: .settingsChanged)

View File

@@ -149,11 +149,15 @@ class MainViewModel
withObservationTracking { withObservationTracking {
_ = nowPlayingViewModel.volume _ = nowPlayingViewModel.volume
} onChange: { [weak self] in } onChange: { [weak self] in
Task {
guard let self else { return } guard let self else { return }
let isRefreshing = isRefreshingFromAPI
Task {
if !isRefreshing {
await self.withModificationsViaAPI { api in await self.withModificationsViaAPI { api in
try await api.setVolume(self.nowPlayingViewModel.volume) try await api.setVolume(self.nowPlayingViewModel.volume)
} }
}
await MainActor.run { self.observeNowPlayingModel() } await MainActor.run { self.observeNowPlayingModel() }
} }

View File

@@ -12,7 +12,11 @@ struct NowPlayingMiniView: View {
let onTap: () -> Void let onTap: () -> Void
@GestureState private var tapGestureState = false @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 { var body: some View {
let playPauseImageName = model.isPlaying ? "pause.fill" : "play.fill" let playPauseImageName = model.isPlaying ? "pause.fill" : "play.fill"
@@ -26,14 +30,14 @@ struct NowPlayingMiniView: View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let title = model.title { if let title = model.title, !title.isEmpty {
Text(title) Text(title)
.font(.caption) .font(.caption)
.lineLimit(1) .lineLimit(1)
.bold() .bold()
} }
if let subtitle = model.subtitle { if let subtitle = model.subtitle, !subtitle.isEmpty {
Text(subtitle) Text(subtitle)
.lineLimit(1) .lineLimit(1)
.font(.caption) .font(.caption)

View File

@@ -175,7 +175,10 @@ struct AddServerView: View
_ = try await api.fetchNowPlayingInfo() _ = try await api.fetchNowPlayingInfo()
self.validationState = .valid self.validationState = .valid
if validationURL != serverURL {
self.serverURL = self.validationURL self.serverURL = self.validationURL
}
} catch { } catch {
print("Validation failed: \(error)") print("Validation failed: \(error)")