better handling of connection errors
This commit is contained in:
@@ -9,7 +9,7 @@ import Foundation
|
||||
|
||||
struct MediaItem: Codable
|
||||
{
|
||||
let filename: String
|
||||
let filename: String?
|
||||
let title: String?
|
||||
let id: Int
|
||||
|
||||
@@ -109,40 +109,65 @@ struct API
|
||||
.post()
|
||||
}
|
||||
|
||||
public func events() async throws -> AsyncStream<Event> {
|
||||
public func events() async throws -> AsyncStream<StreamEvent> {
|
||||
return AsyncStream { continuation in
|
||||
let url = request()
|
||||
.path("/events")
|
||||
.websocket()
|
||||
|
||||
let websocketTask = URLSession.shared.webSocketTask(with: url)
|
||||
websocketTask.resume()
|
||||
var websocketTask: URLSessionWebSocketTask = spawnWebsocketTask(with: continuation)
|
||||
|
||||
Task {
|
||||
do {
|
||||
let event = { (data: Data) in
|
||||
try JSONDecoder().decode(Event.self, from: data)
|
||||
}
|
||||
while true {
|
||||
try await Task.sleep(for: .seconds(5))
|
||||
|
||||
while websocketTask.state == .running {
|
||||
switch try await websocketTask.receive() {
|
||||
case .string(let string):
|
||||
let event = try event(string.data(using: .utf8)!)
|
||||
continuation.yield(event)
|
||||
case .data(let data):
|
||||
let event = try event(data)
|
||||
continuation.yield(event)
|
||||
default:
|
||||
break
|
||||
websocketTask.sendPing { error in
|
||||
if let error {
|
||||
print("Ping error: \(error). Trying to reconnect.")
|
||||
continuation.yield(.error(.websocketError(error)))
|
||||
websocketTask = spawnWebsocketTask(with: continuation)
|
||||
} else {
|
||||
continuation.yield(.event(Event(type: .receivedWebsocketPong)))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Websocket Error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func spawnWebsocketTask(
|
||||
with continuation: AsyncStream<StreamEvent>.Continuation
|
||||
) -> URLSessionWebSocketTask
|
||||
{
|
||||
let url = request()
|
||||
.path("/events")
|
||||
.websocket()
|
||||
|
||||
let websocketTask = URLSession.shared.webSocketTask(with: url)
|
||||
websocketTask.resume()
|
||||
|
||||
Task {
|
||||
do {
|
||||
let event = { (data: Data) in
|
||||
try JSONDecoder().decode(Event.self, from: data)
|
||||
}
|
||||
|
||||
while websocketTask.state == .running {
|
||||
switch try await websocketTask.receive() {
|
||||
case .string(let string):
|
||||
let event = try event(string.data(using: .utf8)!)
|
||||
continuation.yield(.event(event))
|
||||
case .data(let data):
|
||||
let event = try event(data)
|
||||
continuation.yield(.event(event))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Websocket Error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
return websocketTask
|
||||
}
|
||||
|
||||
private func request() -> RequestBuilder {
|
||||
RequestBuilder(url: baseURL)
|
||||
}
|
||||
@@ -152,6 +177,12 @@ struct API
|
||||
enum Error: Swift.Error
|
||||
{
|
||||
case apiNotConfigured
|
||||
case websocketError(Swift.Error)
|
||||
}
|
||||
|
||||
enum StreamEvent {
|
||||
case event(Event)
|
||||
case error(API.Error)
|
||||
}
|
||||
|
||||
struct Event: Decodable
|
||||
@@ -169,6 +200,9 @@ struct API
|
||||
case favoritesUpdate = "favorites_update"
|
||||
case metadataUpdate = "metadata_update"
|
||||
case mpdUpdate = "mpd_update"
|
||||
|
||||
// Private UI events
|
||||
case receivedWebsocketPong
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,14 @@ extension ContentView
|
||||
do {
|
||||
if what.contains(.nowPlaying) {
|
||||
let nowPlaying = try await api.fetchNowPlayingInfo()
|
||||
model.nowPlayingViewModel.title = nowPlaying.playingItem?.title ?? "??"
|
||||
model.nowPlayingViewModel.subtitle = nowPlaying.playingItem?.filename ?? "??"
|
||||
if let nowPlayingItem = nowPlaying.playingItem, let title = nowPlayingItem.title {
|
||||
model.nowPlayingViewModel.title = title
|
||||
model.nowPlayingViewModel.subtitle = nowPlayingItem.filename ?? ""
|
||||
} else {
|
||||
model.nowPlayingViewModel.title = "(Not Playing)"
|
||||
model.nowPlayingViewModel.subtitle = ""
|
||||
}
|
||||
|
||||
model.nowPlayingViewModel.isPlaying = !nowPlaying.isPaused
|
||||
model.nowPlayingViewModel.volume = Double(nowPlaying.volume) / 100.0
|
||||
model.playlistModel.isPlaying = !nowPlaying.isPaused
|
||||
@@ -86,14 +92,17 @@ extension ContentView
|
||||
PlaylistItem(
|
||||
index: idx,
|
||||
id: String(mediaItem.id),
|
||||
title: mediaItem.title ?? mediaItem.filename,
|
||||
filename: mediaItem.filename,
|
||||
title: mediaItem.title ?? mediaItem.filename ?? "<null>",
|
||||
filename: mediaItem.filename ?? "<null>",
|
||||
isCurrent: mediaItem.current ?? false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
model.connectionError = nil
|
||||
} catch {
|
||||
print("Error refreshing content: \(error)")
|
||||
model.connectionError = error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +110,14 @@ extension ContentView
|
||||
guard let api = model.api else { return }
|
||||
|
||||
do {
|
||||
for await event in try await api.events() {
|
||||
print("Got event: \(event.type)")
|
||||
await handle(event: event)
|
||||
for await streamEvent in try await api.events() {
|
||||
switch streamEvent {
|
||||
case .event(let event):
|
||||
model.connectionError = nil
|
||||
await handle(event: event)
|
||||
case .error(let error):
|
||||
model.connectionError = error
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Events error: \(error)")
|
||||
@@ -123,6 +137,13 @@ extension ContentView
|
||||
case .mpdUpdate:
|
||||
await refresh([.playlist, .nowPlaying])
|
||||
|
||||
case .receivedWebsocketPong:
|
||||
// This means we're online.
|
||||
if model.connectionError != nil {
|
||||
model.connectionError = nil
|
||||
await refresh([.playlist, .nowPlaying])
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -134,6 +155,8 @@ class MainViewModel
|
||||
{
|
||||
var api = API.fromSettings()
|
||||
|
||||
var connectionError: Error? = nil
|
||||
|
||||
var playlistModel = PlaylistViewModel()
|
||||
var nowPlayingViewModel = NowPlayingViewModel()
|
||||
var addMediaViewModel = AddMediaBarViewModel()
|
||||
@@ -164,12 +187,10 @@ struct MainView: View
|
||||
VStack {
|
||||
NowPlayingView(model: model.nowPlayingViewModel)
|
||||
.padding()
|
||||
.disabled(showConfigurationDialog)
|
||||
.disabled(showConfigurationDialog || model.connectionError != nil)
|
||||
|
||||
if showConfigurationDialog {
|
||||
Spacer()
|
||||
|
||||
ContentUnavailableView {
|
||||
ContentPlaceholderView {
|
||||
Image(systemName: "server.rack")
|
||||
Text(.notConfigured)
|
||||
} actions: {
|
||||
@@ -179,8 +200,11 @@ struct MainView: View
|
||||
Text(.settings)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
} else if model.connectionError != nil {
|
||||
ContentPlaceholderView {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
Text(.connectionError)
|
||||
}
|
||||
} else {
|
||||
PlaylistView(model: model.playlistModel)
|
||||
.frame(maxWidth: 640.0)
|
||||
@@ -199,3 +223,26 @@ struct MainView: View
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentPlaceholderView<Label, Actions>: View
|
||||
where Label: View, Actions: View
|
||||
{
|
||||
let label: Label
|
||||
let actions: Actions
|
||||
|
||||
init(@ViewBuilder label: () -> Label, @ViewBuilder actions: () -> Actions = { EmptyView() }) {
|
||||
self.label = label()
|
||||
self.actions = actions()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Spacer()
|
||||
|
||||
ContentUnavailableView {
|
||||
label
|
||||
.imageScale(.large)
|
||||
} actions: { actions }
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CONNECTION_ERROR" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Connection Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DONE" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@@ -21,4 +21,5 @@ extension LocalizedStringKey
|
||||
static let configuration = LocalizedStringKey("CONFIGURATION")
|
||||
static let validating = LocalizedStringKey("VALIDATING")
|
||||
static let general = LocalizedStringKey("GENERAL")
|
||||
static let connectionError = LocalizedStringKey("CONNECTION_ERROR")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user