diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index b00b15a1..698dbdf8 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -242,6 +242,9 @@ C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; }; C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; + E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; + E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; + E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.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 */; }; @@ -512,6 +515,7 @@ 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 = ""; }; 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 = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; + E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = ""; }; E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = ""; }; E13DD3BC27163C63009D4DAF /* EmailHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailHelper.swift; sourceTree = ""; }; E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -1164,9 +1168,10 @@ E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */ = { isa = PBXGroup; children = ( - 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */, - E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */, E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */, + E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */, + 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */, + E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */, ); path = JellyfinAPIExtensions; @@ -1595,6 +1600,7 @@ 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */, 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */, + E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E13DD4032717EE79009D4DAF /* UserListCoordinator.swift in Sources */, @@ -1716,6 +1722,7 @@ E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */, + E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, @@ -1766,6 +1773,7 @@ 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */, 6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */, E1AD105926D9A543003E4A08 /* LazyView.swift in Sources */, + E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */, 628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */, E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */, diff --git a/JellyfinPlayer/Views/ConnectToServerView.swift b/JellyfinPlayer/Views/ConnectToServerView.swift index 8039407e..221feb48 100644 --- a/JellyfinPlayer/Views/ConnectToServerView.swift +++ b/JellyfinPlayer/Views/ConnectToServerView.swift @@ -63,8 +63,8 @@ struct ConnectToServerView: View { .headerProminence(.increased) } .alert(item: $viewModel.errorMessage) { _ in - Alert(title: Text("\(viewModel.errorMessage?.code ?? -1)\n\(viewModel.errorMessage?.title ?? "Error")"), - message: Text(viewModel.errorMessage?.displayMessage ?? "Error"), + Alert(title: Text(viewModel.alertTitle), + message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"), dismissButton: .cancel()) } .navigationTitle("Connect") diff --git a/JellyfinPlayer/Views/UserSignInView.swift b/JellyfinPlayer/Views/UserSignInView.swift index 8afde7dd..81110c28 100644 --- a/JellyfinPlayer/Views/UserSignInView.swift +++ b/JellyfinPlayer/Views/UserSignInView.swift @@ -45,6 +45,11 @@ struct UserSignInView: View { Text("Sign In to \(viewModel.server.name)") } } + .alert(item: $viewModel.errorMessage) { _ in + Alert(title: Text(viewModel.alertTitle), + message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"), + dismissButton: .cancel()) + } .navigationTitle("Sign In") } } diff --git a/Shared/Errors/ErrorMessage.swift b/Shared/Errors/ErrorMessage.swift index fab486f7..0f14fe59 100644 --- a/Shared/Errors/ErrorMessage.swift +++ b/Shared/Errors/ErrorMessage.swift @@ -16,6 +16,10 @@ struct ErrorMessage: Identifiable { let title: String let displayMessage: String let logConstructor: LogConstructor + + // Chosen value such that if an error has this code, don't show the code to the UI + // This was chosen because of its unlikelyhood to ever be used + static let noShowErrorCode = -69420 var id: String { return "\(code)\(title)\(logConstructor.message)" diff --git a/Shared/Extensions/JellyfinAPIExtensions/JellyfinAPIError.swift b/Shared/Extensions/JellyfinAPIExtensions/JellyfinAPIError.swift new file mode 100644 index 00000000..f74a5a3c --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/JellyfinAPIError.swift @@ -0,0 +1,23 @@ +// + /* + * 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 + +struct JellyfinAPIError: Error { + + private let message: String + + init(_ message: String) { + self.message = message + } + + var localizedDescription: String { + return message + } +} diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index 8d776c80..867cfac1 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -75,16 +75,30 @@ final class SessionManager { JellyfinAPI.basePath = uri return SystemAPI.getPublicSystemInfo() - .map({ response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in + .tryMap({ response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in + let transaction = SwiftfinStore.dataStack.beginUnsafe() let newServer = transaction.create(Into()) - newServer.uri = response.localAddress ?? "SfUri" - newServer.name = response.serverName ?? "SfServerName" - newServer.id = response.id ?? "" - newServer.os = response.operatingSystem ?? "SfOS" - newServer.version = response.version ?? "SfVersion" + + guard let uri = response.localAddress, + let name = response.serverName, + let id = response.id, + let os = response.operatingSystem, + let version = response.version else { throw JellyfinAPIError("Missing server data from network call") } + + newServer.uri = uri + newServer.name = name + newServer.id = id + newServer.os = os + newServer.version = version newServer.users = [] + // Check for existing server on device + if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", newServer.id)]) { + throw SwiftfinStore.Errors.existingServer(existingServer.state) + } + return (newServer, transaction) }) .handleEvents(receiveOutput: { (_, transaction) in @@ -100,17 +114,29 @@ final class SessionManager { func loginUser(server: SwiftfinStore.State.Server, username: String, password: String) -> AnyPublisher { setAuthHeader(with: "") + JellyfinAPI.basePath = server.uri + return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) - .map({ response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in + .tryMap({ response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in - guard let accessToken = response.accessToken else { fatalError("Received successful user with no access token") } + guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") } let transaction = SwiftfinStore.dataStack.beginUnsafe() let newUser = transaction.create(Into()) - newUser.username = response.user?.name ?? "SfUsername" - newUser.id = response.user?.id ?? "SfID" + + guard let username = response.user?.name, + let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") } + + newUser.username = username + newUser.id = id newUser.appleTVID = "" + // Check for existing user on device + if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", newUser.id)]) { + throw SwiftfinStore.Errors.existingUser(existingUser.state) + } + let newAccessToken = transaction.create(Into()) newAccessToken.value = accessToken newUser.accessToken = newAccessToken @@ -156,6 +182,14 @@ final class SessionManager { SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) } + func delete(user: SwiftfinStore.State.User) { + + } + + func delete(server: SwiftfinStore.State.Server) { + + } + private func setAuthHeader(with accessToken: String) { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String var deviceName = UIDevice.current.name diff --git a/Shared/SwiftfinStore/SwiftfinStore.swift b/Shared/SwiftfinStore/SwiftfinStore.swift index f97ef347..1b6a4809 100644 --- a/Shared/SwiftfinStore/SwiftfinStore.swift +++ b/Shared/SwiftfinStore/SwiftfinStore.swift @@ -119,6 +119,13 @@ enum SwiftfinStore { } } + // MARK: Errors + enum Errors { + case existingServer(State.Server) + case existingUser(State.User) + } + + // MARK: dataStack static let dataStack: DataStack = { let schema = CoreStoreSchema(modelVersion: "V1", entities: [ @@ -138,3 +145,24 @@ enum SwiftfinStore { return _dataStack }() } + +extension SwiftfinStore.Errors: LocalizedError { + + var title: String { + switch self { + case .existingServer(_): + return "Existing Server" + case .existingUser(_): + return "Existing User" + } + } + + var errorDescription: String? { + switch self { + case .existingServer(let server): + return "Server \(server.name) already exists with same server ID" + case .existingUser(let user): + return "User \(user.username) already exists with same user ID" + } + } +} diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index c77297e1..e2c08c8b 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -18,6 +18,15 @@ final class ConnectToServerViewModel: ViewModel { @Published var discoveredServers: Set = [] @Published var searching = false private let discovery = ServerDiscovery() + + var alertTitle: String { + var message: String = "" + if errorMessage?.code != ErrorMessage.noShowErrorCode { + message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n") + } + message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")") + return message + } func connectToServer(uri: String) { #if targetEnvironment(simulator) diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index f1da84b2..5310a267 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -20,9 +20,18 @@ final class UserSignInViewModel: ViewModel { self.server = server } + var alertTitle: String { + var message: String = "" + if errorMessage?.code != ErrorMessage.noShowErrorCode { + message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n") + } + message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")") + return message + } + func login(username: String, password: String) { LogManager.shared.log.debug("Attempting to login to server at \"\(server.uri)\"", tag: "login") - LogManager.shared.log.debug("username == \"\": \(username), password == \"\": \(password)", tag: "login") + LogManager.shared.log.debug("username: \(username), password: \(password)", tag: "login") SessionManager.main.loginUser(server: server, username: username, password: password) .trackActivity(loading) diff --git a/Shared/ViewModels/ViewModel.swift b/Shared/ViewModels/ViewModel.swift index 984a53cb..001c6fed 100644 --- a/Shared/ViewModels/ViewModel.swift +++ b/Shared/ViewModels/ViewModel.swift @@ -29,11 +29,12 @@ class ViewModel: ObservableObject { case .finished: break case .failure(let error): - if let errorResponse = error as? ErrorResponse { - + let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line) + + switch error { + case is ErrorResponse: let networkError: NetworkError - let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line) - + let errorResponse = error as! ErrorResponse switch errorResponse { case .error(-1, _, _, _): networkError = .URLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor) @@ -51,7 +52,55 @@ class ViewModel: ObservableObject { self.errorMessage = networkError.errorMessage networkError.logMessage() + + case is SwiftfinStore.Errors: + let swiftfinError = error as! SwiftfinStore.Errors + let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode, + title: swiftfinError.title, + displayMessage: swiftfinError.errorDescription ?? "", + logConstructor: logConstructor) + self.errorMessage = errorMessage + LogManager.shared.log.error("Request failed: \(swiftfinError.errorDescription ?? "")") + + default: + let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode, + title: "Generic Error", + displayMessage: error.localizedDescription, + logConstructor: logConstructor) + self.errorMessage = genericErrorMessage + LogManager.shared.log.error("Request failed: Generic error - \(error.localizedDescription)") } + +// if let errorResponse = error as? ErrorResponse { +// +// let networkError: NetworkError +// +// 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() +// } else { +// let generalErrorMessage = ErrorMessage(code: 0, +// title: "Error", +// displayMessage: error.localizedDescription, +// logConstructor: logConstructor) +// +// self.errorMessage = generalErrorMessage +// LogManager.shared.log.error("Request failed: General error - \(error.localizedDescription)") +// } } } }