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

View File

@ -169,7 +169,9 @@ struct ConnectToServerView: View {
viewModel.passwordSubject.send(password)
}
.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: ""))
}

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
final class ConnectToServerViewModel: ViewModel {
@Published
var isConnectedServer = false
@Published var isConnectedServer = false
var uriSubject = CurrentValueSubject<String, Never>("")
var usernameSubject = CurrentValueSubject<String, Never>("")
var passwordSubject = CurrentValueSubject<String, Never>("")
@Published
var lastPublicUsers = [UserDto]()
@Published
var publicUsers = [UserDto]()
@Published
var selectedPublicUser = UserDto()
@Published var lastPublicUsers = [UserDto]()
@Published var publicUsers = [UserDto]()
@Published var selectedPublicUser = UserDto()
private let discovery: ServerDiscovery = ServerDiscovery()
@Published var servers: [ServerDiscovery.ServerLookupResponse] = []
@ -41,7 +38,7 @@ final class ConnectToServerViewModel: ViewModel {
UserAPI.getPublicUsers()
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion)
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
self.publicUsers = response
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")
ServerEnvironment.current.create(with: uriSubject.value)
.trackActivity(loading)
.sink(receiveCompletion: { result in
switch result {
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
}
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", completion: completion)
}, receiveValue: { _ in
LogManager.shared.log.debug("Connected to server at \"\(self.uriSubject.value)\"", tag: "connectToServer")
self.getPublicUsers()
@ -112,25 +100,7 @@ final class ConnectToServerViewModel: ViewModel {
SessionManager.current.login(username: usernameSubject.value, password: passwordSubject.value)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
switch 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
}
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", completion: completion)
}, receiveValue: { _ in
})

View File

@ -12,6 +12,7 @@ import Foundation
import JellyfinAPI
class DetailItemViewModel: ViewModel {
@Published var item: 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])
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.similarItems = response.items ?? []
})
@ -43,7 +44,7 @@ class DetailItemViewModel: ViewModel {
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isWatched = false
})
@ -52,7 +53,7 @@ class DetailItemViewModel: ViewModel {
PlaystateAPI.markPlayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isWatched = true
})
@ -65,7 +66,7 @@ class DetailItemViewModel: ViewModel {
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isFavorited = false
})
@ -74,7 +75,7 @@ class DetailItemViewModel: ViewModel {
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isFavorited = true
})

View File

@ -14,14 +14,10 @@ import JellyfinAPI
final class HomeViewModel: ViewModel {
@Published
var librariesShowRecentlyAddedIDs = [String]()
@Published
var libraries = [BaseItemDto]()
@Published
var resumeItems = [BaseItemDto]()
@Published
var nextUpItems = [BaseItemDto]()
@Published var librariesShowRecentlyAddedIDs = [String]()
@Published var libraries = [BaseItemDto]()
@Published var resumeItems = [BaseItemDto]()
@Published var nextUpItems = [BaseItemDto]()
// temp
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!)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion)
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
response.items!.forEach { item in
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()
.trackActivity(self.loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion)
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
self.libraries.forEach { library in
if !(response.configuration?.latestItemsExcludes?.contains(library.id!))! {
@ -67,7 +63,7 @@ final class HomeViewModel: ViewModel {
mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion)
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items")
self.resumeItems = response.items ?? []
@ -78,7 +74,7 @@ final class HomeViewModel: ViewModel {
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion)
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items")
self.nextUpItems = response.items ?? []

View File

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

View File

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

View File

@ -11,8 +11,8 @@ import Foundation
import JellyfinAPI
final class LibraryListViewModel: ViewModel {
@Published
var libraries = [BaseItemDto]()
@Published var libraries = [BaseItemDto]()
// temp
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")
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestCompletion(completion: completion)
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
self.libraries.append(contentsOf: response.items ?? [])
})

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import Foundation
import JellyfinAPI
final class SeriesItemViewModel: DetailItemViewModel {
@Published var seasons = [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)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
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)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.seasons = response.items ?? []
LogManager.shared.log.debug("Retrieved \(String(self?.seasons.count ?? 0)) seasons")

View File

@ -12,44 +12,46 @@ import Foundation
import ActivityIndicator
import JellyfinAPI
typealias ErrorMessage = String
extension ErrorMessage: Identifiable {
public var id: String {
self
}
}
class ViewModel: ObservableObject {
var cancellables = Set<AnyCancellable>()
@Published
var isLoading = true
@Published var isLoading = true
@Published var errorMessage: ErrorMessage?
let loading = ActivityIndicator()
@Published
var errorMessage: ErrorMessage?
var cancellables = Set<AnyCancellable>()
init() {
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 {
case .finished:
break
case .failure(let error):
if let err = error as? ErrorResponse {
switch err {
case .error(401, _, _, _):
LogManager.shared.log.error("Request failed: User unauthorized, server returned a 401 error code.")
self.errorMessage = err.localizedDescription
SessionManager.current.logout()
case .error:
LogManager.shared.log.error("Request failed.")
LogManager.shared.log.error((err as NSError).debugDescription)
self.errorMessage = err.localizedDescription
if let errorResponse = error as? ErrorResponse {
let networkError: NetworkError
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line)
switch errorResponse {
case .error(-1, _, _, _):
networkError = .URLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
// Use the errorResponse description for debugging, rather than the user-facing friendly description which may not be implemented
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)")
}
self.errorMessage = networkError.errorMessage
networkError.logMessage()
}
}
break
}
}
}