Merge pull request #136 from LePips/general-error-messages

Implement General Errors
This commit is contained in:
aiden 3 2021-08-12 18:54:59 -04:00 committed by GitHub
commit c5d346a261
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 330 additions and 134 deletions

View File

@ -197,6 +197,15 @@
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; };
AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; };
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; };
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; };
E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; };
E1FCD09926C4F358007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -376,6 +385,9 @@
D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = "<group>"; }; D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = "<group>"; };
DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.debug.xcconfig"; sourceTree = "<group>"; }; DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.debug.xcconfig"; sourceTree = "<group>"; };
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = "<group>"; };
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
EBFE1F64394BCC2EFFF1610D /* Pods_JellyfinPlayer_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EBFE1F64394BCC2EFFF1610D /* Pods_JellyfinPlayer_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -557,6 +569,7 @@
535870752669D60C00D05A09 /* Shared */ = { 535870752669D60C00D05A09 /* Shared */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E1FCD08E26C466F3007C8DCF /* Errors */,
091B5A852683142E00D78B61 /* ServerLocator */, 091B5A852683142E00D78B61 /* ServerLocator */,
62EC352A26766657000E9F2D /* Singleton */, 62EC352A26766657000E9F2D /* Singleton */,
532175392671BCED005491E6 /* ViewModels */, 532175392671BCED005491E6 /* ViewModels */,
@ -760,6 +773,16 @@
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E1FCD08E26C466F3007C8DCF /* Errors */ = {
isa = PBXGroup;
children = (
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */,
E131691626C583BC0074BFEE /* LogConstructor.swift */,
E1FCD09526C47118007C8DCF /* ErrorMessage.swift */,
);
path = Errors;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -1065,6 +1088,7 @@
53ABFDDE267974E300886593 /* SplashView.swift in Sources */, 53ABFDDE267974E300886593 /* SplashView.swift in Sources */,
53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */, 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */,
62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */,
E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */,
53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */, 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */,
536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */,
62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
@ -1072,6 +1096,7 @@
53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */,
536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */,
091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */,
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */,
62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */,
53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */, 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */,
@ -1120,6 +1145,7 @@
53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */,
5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */, 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */,
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */, 535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */,
09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */, 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */,
); );
@ -1144,6 +1170,7 @@
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */,
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */, 53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */,
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
@ -1155,6 +1182,7 @@
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */, 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */,
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */, 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */,
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */, 532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */,
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
@ -1188,6 +1216,7 @@
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */, 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */,
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */, 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */, 625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */,
@ -1206,8 +1235,11 @@
6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */, 6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */,
628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */, 628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */,
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */, 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */,
E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */,
628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */, 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */,
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */, 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */,
E1FCD09926C4F358007C8DCF /* NetworkError.swift in Sources */,
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */,
62EC353226766849000E9F2D /* SessionManager.swift in Sources */, 62EC353226766849000E9F2D /* SessionManager.swift in Sources */,
536D3D79267BD5D00004248C /* ViewModel.swift in Sources */, 536D3D79267BD5D00004248C /* ViewModel.swift in Sources */,
); );

View File

@ -169,7 +169,9 @@ struct ConnectToServerView: View {
viewModel.passwordSubject.send(password) viewModel.passwordSubject.send(password)
} }
.alert(item: $viewModel.errorMessage) { _ in .alert(item: $viewModel.errorMessage) { _ in
Alert(title: Text("Error"), message: Text($viewModel.errorMessage.wrappedValue!), dismissButton: .default(Text("Try again"))) Alert(title: Text("\(viewModel.errorMessage?.code ?? -1)\n\(viewModel.errorMessage?.title ?? "Error")"),
message: Text(viewModel.errorMessage?.displayMessage ?? "Error"),
dismissButton: .cancel())
} }
.navigationTitle(NSLocalizedString("Connect to Server", comment: "")) .navigationTitle(NSLocalizedString("Connect to Server", comment: ""))
} }

View File

@ -0,0 +1,31 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import JellyfinAPI
struct ErrorMessage: Identifiable {
let code: Int
let title: String
let displayMessage: String
let logConstructor: LogConstructor
var id: String {
return "\(code)\(title)\(logConstructor.message)"
}
/// If the custom displayMessage is `nil`, it will be set to the given logConstructor's message
init(code: Int, title: String, displayMessage: String?, logConstructor: LogConstructor) {
self.code = code
self.title = title
self.displayMessage = displayMessage ?? logConstructor.message
self.logConstructor = logConstructor
}
}

View File

@ -0,0 +1,20 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import JellyfinAPI
struct LogConstructor {
var message: String
let tag: String
let level: LogLevel
let function: String
let file: String
let line: UInt
}

View File

@ -0,0 +1,152 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import JellyfinAPI
/**
The implementation of the network errors here are a temporary measure.
It is very repetitive, messy, and doesn't fulfill the entire specification of "error reporting".
The specific kind of errors here should be created and surfaced from within JellyfinAPI on API calls.
*/
enum NetworkError: Error {
/// For the case that the ErrorResponse object has a code of -1
case URLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
/// For the case that the ErrorRespones object has a code of -2
case HTTPURLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
/// For the case that the ErrorResponse object has a positive code
case JellyfinError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
var errorMessage: ErrorMessage {
switch self {
case .URLError(let response, let displayMessage, let logConstructor):
return NetworkError.parseURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
case .HTTPURLError(let response, let displayMessage, let logConstructor):
return NetworkError.parseHTTPURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
case .JellyfinError(let response, let displayMessage, let logConstructor):
return NetworkError.parseJellyfinError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
}
}
func logMessage() {
let logConstructor = errorMessage.logConstructor
let logFunction: (@autoclosure () -> String, String, String, String, UInt) -> Void
switch logConstructor.level {
case .trace:
logFunction = LogManager.shared.log.trace
case .debug:
logFunction = LogManager.shared.log.debug
case .information:
logFunction = LogManager.shared.log.info
case .warning:
logFunction = LogManager.shared.log.warning
case .error:
logFunction = LogManager.shared.log.error
case .critical:
logFunction = LogManager.shared.log.critical
case ._none:
logFunction = LogManager.shared.log.debug
}
logFunction(logConstructor.message, logConstructor.tag, logConstructor.function, logConstructor.file, logConstructor.line)
}
private static func parseURLError(from response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) -> ErrorMessage {
let errorMessage: ErrorMessage
var logMessage = "An error has occurred."
var logConstructor = logConstructor
switch response {
case .error(_, _, _, let err):
// These codes are currently referenced from:
// https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes
switch err._code {
case -1001:
logMessage = "Network timed out."
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code,
title: "Timed Out",
displayMessage: displayMessage,
logConstructor: logConstructor)
case -1004:
logMessage = "Cannot connect to host."
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code,
title: "Error",
displayMessage: displayMessage,
logConstructor: logConstructor)
default:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code,
title: "Error",
displayMessage: displayMessage,
logConstructor: logConstructor)
}
}
return errorMessage
}
private static func parseHTTPURLError(from response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) -> ErrorMessage {
let errorMessage: ErrorMessage
let logMessage = "An HTTP URL error has occurred"
var logConstructor = logConstructor
// Not implemented as has not run into one of these errors as time of writing
switch response {
case .error(_, _, _, _):
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: 0,
title: "Error",
displayMessage: displayMessage,
logConstructor: logConstructor)
}
return errorMessage
}
private static func parseJellyfinError(from response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) -> ErrorMessage {
let errorMessage: ErrorMessage
var logMessage = "An error has occurred."
var logConstructor = logConstructor
switch response {
case .error(let code, _, _, _):
// Generic HTTP status codes
switch code {
case 401:
logMessage = "User is unauthorized."
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: code,
title: "Unauthorized",
displayMessage: displayMessage,
logConstructor: logConstructor)
default:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: code,
title: "Error",
displayMessage: displayMessage,
logConstructor: logConstructor)
}
}
return errorMessage
}
}

View File

@ -12,19 +12,16 @@ import Foundation
import JellyfinAPI import JellyfinAPI
final class ConnectToServerViewModel: ViewModel { final class ConnectToServerViewModel: ViewModel {
@Published
var isConnectedServer = false @Published var isConnectedServer = false
var uriSubject = CurrentValueSubject<String, Never>("") var uriSubject = CurrentValueSubject<String, Never>("")
var usernameSubject = CurrentValueSubject<String, Never>("") var usernameSubject = CurrentValueSubject<String, Never>("")
var passwordSubject = CurrentValueSubject<String, Never>("") var passwordSubject = CurrentValueSubject<String, Never>("")
@Published @Published var lastPublicUsers = [UserDto]()
var lastPublicUsers = [UserDto]() @Published var publicUsers = [UserDto]()
@Published @Published var selectedPublicUser = UserDto()
var publicUsers = [UserDto]()
@Published
var selectedPublicUser = UserDto()
private let discovery: ServerDiscovery = ServerDiscovery() private let discovery: ServerDiscovery = ServerDiscovery()
@Published var servers: [ServerDiscovery.ServerLookupResponse] = [] @Published var servers: [ServerDiscovery.ServerLookupResponse] = []
@ -41,7 +38,7 @@ final class ConnectToServerViewModel: ViewModel {
UserAPI.getPublicUsers() UserAPI.getPublicUsers()
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion) self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in }, receiveValue: { response in
self.publicUsers = response self.publicUsers = response
LogManager.shared.log.debug("Received \(String(response.count)) public users.", tag: "getPublicUsers") LogManager.shared.log.debug("Received \(String(response.count)) public users.", tag: "getPublicUsers")
@ -67,17 +64,8 @@ final class ConnectToServerViewModel: ViewModel {
LogManager.shared.log.debug("Attempting to connect to server at \"\(uriSubject.value)\"", tag: "connectToServer") LogManager.shared.log.debug("Attempting to connect to server at \"\(uriSubject.value)\"", tag: "connectToServer")
ServerEnvironment.current.create(with: uriSubject.value) ServerEnvironment.current.create(with: uriSubject.value)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { result in .sink(receiveCompletion: { completion in
switch result { self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", completion: completion)
case let .failure(error):
let err = error as NSError
LogManager.shared.log.critical("Error connecting to server at \"\(self.uriSubject.value)\"", tag: "connectToServer")
LogManager.shared.log.critical(err.debugDescription, tag: "login")
self.errorMessage = error.localizedDescription
break
default:
break
}
}, receiveValue: { _ in }, receiveValue: { _ in
LogManager.shared.log.debug("Connected to server at \"\(self.uriSubject.value)\"", tag: "connectToServer") LogManager.shared.log.debug("Connected to server at \"\(self.uriSubject.value)\"", tag: "connectToServer")
self.getPublicUsers() self.getPublicUsers()
@ -112,25 +100,7 @@ final class ConnectToServerViewModel: ViewModel {
SessionManager.current.login(username: usernameSubject.value, password: passwordSubject.value) SessionManager.current.login(username: usernameSubject.value, password: passwordSubject.value)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
switch completion { self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", completion: completion)
case .finished:
break
case .failure(let error):
if let err = error as? ErrorResponse {
switch err {
case .error(401, _, _, _):
LogManager.shared.log.critical("Error connecting to server at \"\(self.uriSubject.value)\"", tag: "login")
LogManager.shared.log.critical("User provided invalid credentials, server returned a 401 error.", tag: "login")
self.errorMessage = "Invalid credentials"
case .error:
let err = error as NSError
LogManager.shared.log.critical("Error logging in to server at \"\(self.uriSubject.value)\"", tag: "login")
LogManager.shared.log.critical(err.debugDescription, tag: "login")
self.errorMessage = err.localizedDescription
}
}
break
}
}, receiveValue: { _ in }, receiveValue: { _ in
}) })

View File

@ -12,6 +12,7 @@ import Foundation
import JellyfinAPI import JellyfinAPI
class DetailItemViewModel: ViewModel { class DetailItemViewModel: ViewModel {
@Published var item: BaseItemDto @Published var item: BaseItemDto
@Published var similarItems: [BaseItemDto] = [] @Published var similarItems: [BaseItemDto] = []
@ -31,7 +32,7 @@ class DetailItemViewModel: ViewModel {
LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.current.user.user_id!, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.current.user.user_id!, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
self?.similarItems = response.items ?? [] self?.similarItems = response.items ?? []
}) })
@ -43,7 +44,7 @@ class DetailItemViewModel: ViewModel {
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) PlaystateAPI.markUnplayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in }, receiveValue: { [weak self] _ in
self?.isWatched = false self?.isWatched = false
}) })
@ -52,7 +53,7 @@ class DetailItemViewModel: ViewModel {
PlaystateAPI.markPlayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) PlaystateAPI.markPlayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in }, receiveValue: { [weak self] _ in
self?.isWatched = true self?.isWatched = true
}) })
@ -65,7 +66,7 @@ class DetailItemViewModel: ViewModel {
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in }, receiveValue: { [weak self] _ in
self?.isFavorited = false self?.isFavorited = false
}) })
@ -74,7 +75,7 @@ class DetailItemViewModel: ViewModel {
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in }, receiveValue: { [weak self] _ in
self?.isFavorited = true self?.isFavorited = true
}) })

View File

@ -14,14 +14,10 @@ import JellyfinAPI
final class HomeViewModel: ViewModel { final class HomeViewModel: ViewModel {
@Published @Published var librariesShowRecentlyAddedIDs = [String]()
var librariesShowRecentlyAddedIDs = [String]() @Published var libraries = [BaseItemDto]()
@Published @Published var resumeItems = [BaseItemDto]()
var libraries = [BaseItemDto]() @Published var nextUpItems = [BaseItemDto]()
@Published
var resumeItems = [BaseItemDto]()
@Published
var nextUpItems = [BaseItemDto]()
// temp // temp
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded]) var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])
@ -37,7 +33,7 @@ final class HomeViewModel: ViewModel {
UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id!) UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id!)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion) self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in }, receiveValue: { response in
response.items!.forEach { item in response.items!.forEach { item in
LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")") LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")")
@ -49,7 +45,7 @@ final class HomeViewModel: ViewModel {
UserAPI.getCurrentUser() UserAPI.getCurrentUser()
.trackActivity(self.loading) .trackActivity(self.loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion) self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in }, receiveValue: { response in
self.libraries.forEach { library in self.libraries.forEach { library in
if !(response.configuration?.latestItemsExcludes?.contains(library.id!))! { if !(response.configuration?.latestItemsExcludes?.contains(library.id!))! {
@ -67,7 +63,7 @@ final class HomeViewModel: ViewModel {
mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion) self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in }, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items") LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items")
self.resumeItems = response.items ?? [] self.resumeItems = response.items ?? []
@ -78,7 +74,7 @@ final class HomeViewModel: ViewModel {
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion) self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in }, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items") LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items")
self.nextUpItems = response.items ?? [] self.nextUpItems = response.items ?? []

View File

@ -12,8 +12,8 @@ import Foundation
import JellyfinAPI import JellyfinAPI
final class LatestMediaViewModel: ViewModel { final class LatestMediaViewModel: ViewModel {
@Published
var items = [BaseItemDto]() @Published var items = [BaseItemDto]()
var libraryID: String var libraryID: String
@ -38,7 +38,7 @@ final class LatestMediaViewModel: ViewModel {
enableUserData: true, limit: 12) enableUserData: true, limit: 12)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
self?.items = response self?.items = response
LogManager.shared.log.debug("Retrieved \(String(self?.items.count ?? 0)) items") LogManager.shared.log.debug("Retrieved \(String(self?.items.count ?? 0)) items")

View File

@ -20,25 +20,17 @@ enum FilterType {
} }
final class LibraryFilterViewModel: ViewModel { final class LibraryFilterViewModel: ViewModel {
@Published
var modifiedFilters = LibraryFilters() @Published var modifiedFilters = LibraryFilters()
@Published @Published var possibleGenres = [NameGuidPair]()
var possibleGenres = [NameGuidPair]() @Published var possibleTags = [String]()
@Published @Published var possibleSortOrders = APISortOrder.allCases
var possibleTags = [String]() @Published var possibleSortBys = SortBy.allCases
@Published @Published var possibleItemFilters = ItemFilter.supportedTypes
var possibleSortOrders = APISortOrder.allCases @Published var enabledFilterType: [FilterType]
@Published @Published var selectedSortOrder: APISortOrder = .descending
var possibleSortBys = SortBy.allCases @Published var selectedSortBy: SortBy = .name
@Published
var possibleItemFilters = ItemFilter.supportedTypes
@Published
var enabledFilterType: [FilterType]
@Published
var selectedSortOrder: APISortOrder = .descending
@Published
var selectedSortBy: SortBy = .name
var parentId: String = "" var parentId: String = ""
@ -69,7 +61,7 @@ final class LibraryFilterViewModel: ViewModel {
FilterAPI.getQueryFilters(userId: SessionManager.current.user.user_id!, parentId: self.parentId) FilterAPI.getQueryFilters(userId: SessionManager.current.user.user_id!, parentId: self.parentId)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] queryFilters in }, receiveValue: { [weak self] queryFilters in
guard let self = self else { return } guard let self = self else { return }
self.possibleGenres = queryFilters.genres ?? [] self.possibleGenres = queryFilters.genres ?? []

View File

@ -11,8 +11,8 @@ import Foundation
import JellyfinAPI import JellyfinAPI
final class LibraryListViewModel: ViewModel { final class LibraryListViewModel: ViewModel {
@Published
var libraries = [BaseItemDto]() @Published var libraries = [BaseItemDto]()
// temp // temp
var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: []) var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: [])
@ -27,7 +27,7 @@ final class LibraryListViewModel: ViewModel {
UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id ?? "val was nil") UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id ?? "val was nil")
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion) self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in }, receiveValue: { response in
self.libraries.append(contentsOf: response.items ?? []) self.libraries.append(contentsOf: response.items ?? [])
}) })

View File

@ -87,9 +87,11 @@ final class LibrarySearchViewModel: ViewModel {
enableTotalRecordCount: false, enableTotalRecordCount: false,
enableImages: false) enableImages: false)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: handleAPIRequestCompletion(completion:)) { [weak self] response in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.suggestions = response.items ?? [] self?.suggestions = response.items ?? []
} })
.store(in: &cancellables) .store(in: &cancellables)
} }
@ -100,7 +102,7 @@ final class LibrarySearchViewModel: ViewModel {
includeItemTypes: [ItemType.movie.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) includeItemTypes: [ItemType.movie.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
self?.movieItems = response.items ?? [] self?.movieItems = response.items ?? []
}) })
@ -111,7 +113,7 @@ final class LibrarySearchViewModel: ViewModel {
includeItemTypes: [ItemType.series.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) includeItemTypes: [ItemType.series.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
self?.showItems = response.items ?? [] self?.showItems = response.items ?? []
}) })
@ -122,7 +124,7 @@ final class LibrarySearchViewModel: ViewModel {
includeItemTypes: [ItemType.episode.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) includeItemTypes: [ItemType.episode.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
self?.episodeItems = response.items ?? [] self?.episodeItems = response.items ?? []
}) })

View File

@ -17,21 +17,15 @@ final class LibraryViewModel: ViewModel {
var genre: NameGuidPair? var genre: NameGuidPair?
var studio: NameGuidPair? var studio: NameGuidPair?
@Published @Published var items = [BaseItemDto]()
var items = [BaseItemDto]()
@Published @Published var totalPages = 0
var totalPages = 0 @Published var currentPage = 0
@Published @Published var hasNextPage = false
var currentPage = 0 @Published var hasPreviousPage = false
@Published
var hasNextPage = false
@Published
var hasPreviousPage = false
// temp // temp
@Published @Published var filters: LibraryFilters
var filters: LibraryFilters
var enabledFilterType: [FilterType] { var enabledFilterType: [FilterType] {
if genre == nil { if genre == nil {
@ -76,7 +70,7 @@ final class LibraryViewModel: ViewModel {
enableUserData: true, personIds: personIDs, studioIds: studioIDs, genreIds: genreIDs, enableImages: true) enableUserData: true, personIds: personIDs, studioIds: studioIDs, genreIds: genreIDs, enableImages: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) items in library \(self?.parentID ?? "nil")") LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) items in library \(self?.parentID ?? "nil")")
guard let self = self else { return } guard let self = self else { return }
@ -106,7 +100,7 @@ final class LibraryViewModel: ViewModel {
filters: filters.filters, sortBy: sortBy, tags: filters.tags, filters: filters.filters, sortBy: sortBy, tags: filters.tags,
enableUserData: true, personIds: personIDs, studioIds: studioIDs, genreIds: genreIDs, enableImages: true) enableUserData: true, personIds: personIDs, studioIds: studioIDs, genreIds: genreIDs, enableImages: true)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
guard let self = self else { return } guard let self = self else { return }
let totalPages = ceil(Double(response.totalRecordCount ?? 0) / 100.0) let totalPages = ceil(Double(response.totalRecordCount ?? 0) / 100.0)

View File

@ -12,6 +12,7 @@ import Foundation
import JellyfinAPI import JellyfinAPI
final class SeasonItemViewModel: DetailItemViewModel { final class SeasonItemViewModel: DetailItemViewModel {
@Published var episodes = [BaseItemDto]() @Published var episodes = [BaseItemDto]()
override init(item: BaseItemDto) { override init(item: BaseItemDto) {
@ -28,7 +29,7 @@ final class SeasonItemViewModel: DetailItemViewModel {
seasonId: item.id ?? "") seasonId: item.id ?? "")
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
self?.episodes = response.items ?? [] self?.episodes = response.items ?? []
LogManager.shared.log.debug("Retrieved \(String(self?.episodes.count ?? 0)) episodes") LogManager.shared.log.debug("Retrieved \(String(self?.episodes.count ?? 0)) episodes")

View File

@ -12,6 +12,7 @@ import Foundation
import JellyfinAPI import JellyfinAPI
final class SeriesItemViewModel: DetailItemViewModel { final class SeriesItemViewModel: DetailItemViewModel {
@Published var seasons = [BaseItemDto]() @Published var seasons = [BaseItemDto]()
@Published var nextUpItem: BaseItemDto? @Published var nextUpItem: BaseItemDto?
@ -28,7 +29,7 @@ final class SeriesItemViewModel: DetailItemViewModel {
TvShowsAPI.getNextUp(userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seriesId: self.item.id!, enableUserData: true) TvShowsAPI.getNextUp(userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seriesId: self.item.id!, enableUserData: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
self?.nextUpItem = response.items?.first ?? nil self?.nextUpItem = response.items?.first ?? nil
}) })
@ -58,7 +59,7 @@ final class SeriesItemViewModel: DetailItemViewModel {
TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true) TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
self?.seasons = response.items ?? [] self?.seasons = response.items ?? []
LogManager.shared.log.debug("Retrieved \(String(self?.seasons.count ?? 0)) seasons") LogManager.shared.log.debug("Retrieved \(String(self?.seasons.count ?? 0)) seasons")

View File

@ -12,44 +12,46 @@ import Foundation
import ActivityIndicator import ActivityIndicator
import JellyfinAPI import JellyfinAPI
typealias ErrorMessage = String
extension ErrorMessage: Identifiable {
public var id: String {
self
}
}
class ViewModel: ObservableObject { class ViewModel: ObservableObject {
var cancellables = Set<AnyCancellable>()
@Published @Published var isLoading = true
var isLoading = true @Published var errorMessage: ErrorMessage?
let loading = ActivityIndicator() let loading = ActivityIndicator()
@Published var cancellables = Set<AnyCancellable>()
var errorMessage: ErrorMessage?
init() { init() {
loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables) loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables)
} }
func handleAPIRequestCompletion(completion: Subscribers.Completion<Error>) { func handleAPIRequestError(displayMessage: String? = nil, logLevel: LogLevel = .error, tag: String = "", function: String = #function, file: String = #file, line: UInt = #line, completion: Subscribers.Completion<Error>) {
switch completion { switch completion {
case .finished: case .finished:
break break
case .failure(let error): case .failure(let error):
if let err = error as? ErrorResponse { if let errorResponse = error as? ErrorResponse {
switch err {
case .error(401, _, _, _): let networkError: NetworkError
LogManager.shared.log.error("Request failed: User unauthorized, server returned a 401 error code.") let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line)
self.errorMessage = err.localizedDescription
SessionManager.current.logout() switch errorResponse {
case .error: case .error(-1, _, _, _):
LogManager.shared.log.error("Request failed.") networkError = .URLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
LogManager.shared.log.error((err as NSError).debugDescription) // Use the errorResponse description for debugging, rather than the user-facing friendly description which may not be implemented
self.errorMessage = err.localizedDescription LogManager.shared.log.error("Request failed: URL request failed with error \(networkError.errorMessage.code): \(errorResponse.localizedDescription)")
} case .error(-2, _, _, _):
networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
LogManager.shared.log.error("Request failed: HTTP URL request failed with description: \(errorResponse.localizedDescription)")
default:
networkError = .JellyfinError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
// Able to use user-facing friendly description here since just HTTP status codes
LogManager.shared.log.error("Request failed: \(networkError.errorMessage.code) - \(networkError.errorMessage.title): \(networkError.errorMessage.logConstructor.message)\n\(error.localizedDescription)")
} }
break
self.errorMessage = networkError.errorMessage
networkError.logMessage()
}
} }
} }
} }