Merge pull request #136 from LePips/general-error-messages
Implement General Errors
This commit is contained in:
commit
c5d346a261
|
@ -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 */,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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: ""))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 ?? []
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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 ?? []
|
||||||
|
|
|
@ -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 ?? [])
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 ?? []
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue