Resolve various connection issues
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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,10 +135,14 @@ extension ContentView
|
|||||||
|
|
||||||
case .receivedWebsocketPong:
|
case .receivedWebsocketPong:
|
||||||
// This means we're online.
|
// This means we're online.
|
||||||
if model.connectionError != nil {
|
await clearConnectionErrorIfNecessary()
|
||||||
model.connectionError = nil
|
}
|
||||||
await refresh([.playlist, .nowPlaying, .favorites])
|
}
|
||||||
}
|
|
||||||
|
private func clearConnectionErrorIfNecessary() async {
|
||||||
|
if model.connectionError != nil {
|
||||||
|
model.connectionError = nil
|
||||||
|
await refresh([.playlist, .nowPlaying, .favorites])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,10 +149,14 @@ class MainViewModel
|
|||||||
withObservationTracking {
|
withObservationTracking {
|
||||||
_ = nowPlayingViewModel.volume
|
_ = nowPlayingViewModel.volume
|
||||||
} onChange: { [weak self] in
|
} onChange: { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
let isRefreshing = isRefreshingFromAPI
|
||||||
Task {
|
Task {
|
||||||
guard let self else { return }
|
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() }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -175,7 +175,10 @@ struct AddServerView: View
|
|||||||
_ = try await api.fetchNowPlayingInfo()
|
_ = try await api.fetchNowPlayingInfo()
|
||||||
|
|
||||||
self.validationState = .valid
|
self.validationState = .valid
|
||||||
self.serverURL = self.validationURL
|
|
||||||
|
if validationURL != serverURL {
|
||||||
|
self.serverURL = self.validationURL
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Validation failed: \(error)")
|
print("Validation failed: \(error)")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user