From f1dc7be31dee2a05e069c36a7d4b3e6bfede43ca Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Wed, 14 Jul 2021 17:42:53 -0400 Subject: [PATCH] Add related items, add logging statements. --- JellyfinPlayer.xcodeproj/project.pbxproj | 13 ++++ JellyfinPlayer/EpisodeItemView.swift | 38 +++++++++++ JellyfinPlayer/JellyfinPlayerApp.swift | 72 ++++++++++++++++++++ JellyfinPlayer/MovieItemView.swift | 40 ++++++++++- JellyfinPlayer/VideoPlayer.swift | 40 +++++------ Shared/Singleton/LogManager.swift | 38 ++++++++--- Shared/Singleton/ServerEnvironment.swift | 2 + Shared/Singleton/SessionManager.swift | 2 + Shared/ViewModels/HomeViewModel.swift | 5 ++ Shared/ViewModels/LatestMediaViewModel.swift | 2 + Shared/ViewModels/SeasonItemViewModel.swift | 2 + Shared/ViewModels/SeriesItemViewModel.swift | 3 + Shared/ViewModels/SettingsViewModel.swift | 4 +- Shared/ViewModels/SplashViewModel.swift | 4 +- Shared/ViewModels/ViewModel.swift | 3 + 15 files changed, 230 insertions(+), 38 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 2cdbadef..e93e0da5 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -66,6 +66,8 @@ 53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = 53649AAE269CFAF600A2D8B7 /* Puppy */; }; 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; + 53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; + 53649AB5269D423A00A2D8B7 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = 53649AB4269D423A00A2D8B7 /* Puppy */; }; 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D73267BA8170004248C /* BackgroundManager.swift */; }; @@ -398,6 +400,7 @@ 628B95332670CAEA0091AF3B /* NukeUI in Frameworks */, 628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */, 531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */, + 53649AB5269D423A00A2D8B7 /* Puppy in Frameworks */, 536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */, 628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */, 628B95352670CAEA0091AF3B /* JellyfinAPI in Frameworks */, @@ -780,6 +783,7 @@ 628B95342670CAEA0091AF3B /* JellyfinAPI */, 628B95392670CE250091AF3B /* KeychainSwift */, 536D3D7C267BD5F90004248C /* ActivityIndicator */, + 53649AB4269D423A00A2D8B7 /* Puppy */, ); productName = WidgetExtensionExtension; productReference = 628B95202670CABD0091AF3B /* WidgetExtension.appex */; @@ -1115,6 +1119,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */, 62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */, 6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */, 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */, @@ -1163,6 +1168,7 @@ PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 14.5; @@ -1346,6 +1352,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1406,6 +1413,7 @@ PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1601,6 +1609,11 @@ package = 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */; productName = Puppy; }; + 53649AB4269D423A00A2D8B7 /* Puppy */ = { + isa = XCSwiftPackageProductDependency; + package = 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */; + productName = Puppy; + }; 536D3D7C267BD5F90004248C /* ActivityIndicator */ = { isa = XCSwiftPackageProductDependency; package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index 0d0def5f..b58cd9d0 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -180,6 +180,25 @@ struct EpisodeItemView: View { }.padding(.leading, 16).padding(.trailing, 16) } } + if !(viewModel.similarItems).isEmpty { + Text("More Like This") + .font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16) + ScrollView(.horizontal, showsIndicators: false) { + VStack { + Spacer().frame(height: 8) + HStack { + Spacer().frame(width: 16) + ForEach(viewModel.similarItems, id: \.self) { similarItem in + NavigationLink(destination: LazyView { ItemView(item: similarItem) }) { + PortraitItemView(item: similarItem) + } + Spacer().frame(width: 10) + } + Spacer().frame(width: 16) + } + } + }.padding(.top, -5) + } Spacer().frame(height: 3) } } @@ -357,6 +376,25 @@ struct EpisodeItemView: View { .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } } + if !(viewModel.similarItems).isEmpty { + Text("More Like This") + .font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16) + ScrollView(.horizontal, showsIndicators: false) { + VStack { + Spacer().frame(height: 8) + HStack { + Spacer().frame(width: 16) + ForEach(viewModel.similarItems, id: \.self) { similarItem in + NavigationLink(destination: LazyView { ItemView(item: similarItem) }) { + PortraitItemView(item: similarItem) + } + Spacer().frame(width: 10) + } + Spacer().frame(width: 16) + } + } + }.padding(.top, -5) + } Spacer().frame(height: 195) }.frame(maxHeight: .infinity) } diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift index 29723dcb..d3c2b044 100644 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/JellyfinPlayerApp.swift @@ -6,6 +6,41 @@ */ import SwiftUI +import MessageUI + +// The notification we'll send when a shake gesture happens. +extension UIDevice { + static let deviceDidShakeNotification = Notification.Name(rawValue: "deviceDidShakeNotification") +} + +// Override the default behavior of shake gestures to send our notification instead. +extension UIWindow { + open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + if motion == .motionShake { + NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil) + } + } +} + +// A view modifier that detects shaking and calls a function of our choosing. +struct DeviceShakeViewModifier: ViewModifier { + let action: () -> Void + + func body(content: Content) -> some View { + content + .onAppear() + .onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in + action() + } + } +} + +// A View extension to make the modifier easier to use. +extension View { + func onShake(perform action: @escaping () -> Void) -> some View { + self.modifier(DeviceShakeViewModifier(action: action)) + } +} extension UIDevice { var hasNotch: Bool { @@ -138,6 +173,40 @@ extension View { } } +class EmailHelper: NSObject, MFMailComposeViewControllerDelegate { + public static let shared = EmailHelper() + private override init() { + // + } + + func sendLogs(logURL: URL){ + if !MFMailComposeViewController.canSendMail() { + // Utilities.showErrorBanner(title: "No mail account found", subtitle: "Please setup a mail account") + return //EXIT + } + + let picker = MFMailComposeViewController() + + let fileManager = FileManager() + let data = fileManager.contents(atPath: logURL.path) + + picker.setSubject("SwiftFin Shake Report") + picker.setToRecipients(["Aiden Vigue "]) + picker.addAttachmentData(data!, mimeType: "text/plain", fileName: logURL.lastPathComponent) + picker.mailComposeDelegate = self + + EmailHelper.getRootViewController()?.present(picker, animated: true, completion: nil) + } + + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + EmailHelper.getRootViewController()?.dismiss(animated: true, completion: nil) + } + + static func getRootViewController() -> UIViewController? { + UIApplication.shared.windows.first?.rootViewController + } +} + @main struct JellyfinPlayerApp: App { let persistenceController = PersistenceController.shared @@ -149,6 +218,9 @@ struct JellyfinPlayerApp: App { .withHostingWindow { window in window?.rootViewController = PreferenceUIHostingController(wrappedView: SplashView().environment(\.managedObjectContext, persistenceController.container.viewContext)) } + .onShake { + EmailHelper.shared.sendLogs(logURL: LogManager.shared.logFileURL()) + } } } } diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index f866392a..d5615dad 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -192,7 +192,26 @@ struct MovieItemView: View { }.padding(.leading, 16).padding(.trailing, 16) } } - Spacer().frame(height: 3) + if !(viewModel.similarItems).isEmpty { + Text("More Like This") + .font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16) + ScrollView(.horizontal, showsIndicators: false) { + VStack { + Spacer().frame(height: 8) + HStack { + Spacer().frame(width: 16) + ForEach(viewModel.similarItems, id: \.self) { similarItem in + NavigationLink(destination: LazyView { ItemView(item: similarItem) }) { + PortraitItemView(item: similarItem) + } + Spacer().frame(width: 10) + } + Spacer().frame(width: 16) + } + } + }.padding(.top, -5) + } + Spacer().frame(height: 16) } } } else { @@ -376,6 +395,25 @@ struct MovieItemView: View { .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } } + if !(viewModel.similarItems).isEmpty { + Text("More Like This") + .font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16) + ScrollView(.horizontal, showsIndicators: false) { + VStack { + Spacer().frame(height: 8) + HStack { + Spacer().frame(width: 16) + ForEach(viewModel.similarItems, id: \.self) { similarItem in + NavigationLink(destination: LazyView { ItemView(item: similarItem) }) { + PortraitItemView(item: similarItem) + } + Spacer().frame(width: 10) + } + Spacer().frame(width: 16) + } + } + }.padding(.top, -5) + } Spacer().frame(height: 105) }.frame(maxHeight: .infinity) } diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index f486c24c..333eccbb 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -221,6 +221,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // MARK: Cast methods @IBAction func castButtonPressed(_ sender: Any) { if selectedCastDevice == nil { + LogManager.shared.log.debug("Presenting Cast modal") castDeviceVC = VideoPlayerCastDeviceSelectorView() castDeviceVC?.delegate = self @@ -229,11 +230,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Present the view controller (in a popover). self.present(castDeviceVC!, animated: true) { - print("popover visible, pause playback") self.mediaPlayer.pause() self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) } } else { + LogManager.shared.log.info("Stopping casting session: button was pressed.") castSessionManager.endSessionAndStopCasting(true) selectedCastDevice = nil self.castButton.isEnabled = true @@ -243,6 +244,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } func castPopoverDismissed() { + LogManager.shared.log.debug("Cast modal dismissed") castDeviceVC?.dismiss(animated: true, completion: nil) if playerDestination == .local { self.mediaPlayer.play() @@ -251,7 +253,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } func castDeviceChanged() { + LogManager.shared.log.debug("Cast device changed") if selectedCastDevice != nil { + LogManager.shared.log.debug("New device: \(selectedCastDevice?.friendlyName ?? "UNKNOWN")") playerDestination = .remote castSessionManager.add(self) castSessionManager.startSession(with: selectedCastDevice!) @@ -613,7 +617,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } func startLocalPlaybackEngine(_ fetchCaptions: Bool) { - print("Local playback engine starting.") mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) mediaPlayer.play() sendPlayReport() @@ -621,10 +624,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // 1 second = 10,000,000 ticks var startTicks: Int64 = 0 if remotePositionTicks == 0 { - print("Using server-reported start time") startTicks = manifest.userData?.playbackPositionTicks ?? 0 } else { - print("Using remote-reported start time") startTicks = Int64(remotePositionTicks) } @@ -632,7 +633,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let videoPosition = Double(mediaPlayer.time.intValue / 1000) let secondsScrubbedTo = startTicks / 10_000_000 let offset = secondsScrubbedTo - Int64(videoPosition) - print("Seeking to position: \(secondsScrubbedTo)") if offset > 0 { mediaPlayer.jumpForward(Int32(offset)) } else { @@ -641,8 +641,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if fetchCaptions { - print("Fetching captions.") - // Pause and load captions into memory. mediaPlayer.pause() subtitleTrackArray.forEach { sub in if sub.id != -1 && sub.delivery == .external { @@ -664,8 +662,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe mediaPlayer.pause() mediaPlayer.play() setupTracksForPreferredDefaults() - - print("Local engine started.") } // MARK: VideoPlayerSettings Delegate @@ -820,7 +816,6 @@ extension PlayerViewController: GCKGenericChannelDelegate { "receiverName": castSessionManager.currentCastSession!.device.friendlyName!, "subtitleBurnIn": false ] - print(payload) let jsonData = JSON(payload) jellyfinCastChannel?.sendTextMessage(jsonData.rawString()!, error: nil) @@ -874,23 +869,24 @@ extension PlayerViewController: GCKSessionManagerListener { } func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) { - print("starting session") self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") self.sessionDidStart(manager: sessionManager, didStart: session) } func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) { self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") - print("resuming session") self.sessionDidStart(manager: sessionManager, didStart: session) } func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) { - dump(error) + LogManager.shared.log.error((error as NSError).debugDescription) } func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKCastSession, withError error: Error?) { - print("didEnd") + if(error != nil) { + LogManager.shared.log.error((error! as NSError).debugDescription) + } + playerDestination = .local videoContentView.isHidden = false remoteTimeUpdateTimer?.invalidate() @@ -899,7 +895,6 @@ extension PlayerViewController: GCKSessionManagerListener { } func sessionManager(_ sessionManager: GCKSessionManager, didSuspend session: GCKCastSession, with reason: GCKConnectionSuspendReason) { - print("didSuspend") playerDestination = .local videoContentView.isHidden = false remoteTimeUpdateTimer?.invalidate() @@ -914,27 +909,26 @@ extension PlayerViewController: VLCMediaPlayerDelegate { let currentState: VLCMediaPlayerState = mediaPlayer.state switch currentState { case .stopped : + LogManager.shared.log.debug("Player state changed: STOPPED") break case .ended : + LogManager.shared.log.debug("Player state changed: ENDED") break case .playing : - print("Video is playing") + LogManager.shared.log.debug("Player state changed: PLAYING") sendProgressReport(eventName: "unpause") delegate?.hideLoadingView(self) paused = false - case .paused : - print("Video is paused)") + LogManager.shared.log.debug("Player state changed: PAUSED") paused = true - case .opening : - print("Video is opening)") - + LogManager.shared.log.debug("Player state changed: OPENING") case .buffering : - print("Video is buffering)") + LogManager.shared.log.debug("Player state changed: BUFFERING") delegate?.showLoadingView(self) case .error : - print("Video has error)") + LogManager.shared.log.error("Video had error.") sendStopReport() case .esAdded: mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) diff --git a/Shared/Singleton/LogManager.swift b/Shared/Singleton/LogManager.swift index 146666bc..d380ceb2 100644 --- a/Shared/Singleton/LogManager.swift +++ b/Shared/Singleton/LogManager.swift @@ -10,20 +10,39 @@ import Foundation import Puppy -final class LogManager { +class LogManager { static let shared = LogManager() let log = Puppy() init() { let console = ConsoleLogger("me.vigue.jellyfin.ConsoleLogger") - let fileURL = URL(fileURLWithPath: "./app.log").absoluteURL - let file = try? FileLogger("me.vigue.jellyfin", fileURL: fileURL) + let fileURL = self.getDocumentsDirectory().appendingPathComponent("logs.txt") + let FM = FileManager() + _ = try? FM.removeItem(at: fileURL) + + do { + let file = try FileLogger("me.vigue.jellyfin", fileURL: fileURL) + file.format = LogFormatter(); + log.add(file, withLevel: .debug) + } catch(let err) { + log.error("Couldn't initialize file logger.") + print(err); + } console.format = LogFormatter(); log.add(console, withLevel: .debug) - if(file != nil) { - file!.format = LogFormatter(); - log.add(file!, withLevel: .debug) - } + log.info("Logger initialized.") + } + + func logFileURL() -> URL { + return self.getDocumentsDirectory().appendingPathComponent("logs.txt") + } + + func getDocumentsDirectory() -> URL { + // find all possible documents directories for this user + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + + // just send back the first one, which ought to be the only one + return paths[0] } } @@ -31,8 +50,7 @@ class LogFormatter: LogFormattable { func formatMessage(_ level: LogLevel, message: String, tag: String, function: String, file: String, line: UInt, swiftLogInfo: [String : String], label: String, date: Date, threadID: UInt64) -> String { - let date = dateFormatter(date) - let file = shortFileName(file) - return "\(date) \(threadID) [\(level.emoji) \(level)] \(file)#L.\(line) \(function) \(message)" + let file = shortFileName(file).replacingOccurrences(of: ".swift", with: "") + return " [\(level.emoji) \(level)] \(file)#\(line):\(function) \(message)" } } diff --git a/Shared/Singleton/ServerEnvironment.swift b/Shared/Singleton/ServerEnvironment.swift index 7837a55d..58b2beee 100644 --- a/Shared/Singleton/ServerEnvironment.swift +++ b/Shared/Singleton/ServerEnvironment.swift @@ -27,6 +27,7 @@ final class ServerEnvironment { } func create(with uri: String) -> AnyPublisher { + LogManager.shared.log.debug("Initializing new Server object with raw URI: \"\(uri)\"") var uri = uri if !uri.contains("http") { uri = "https://" + uri @@ -34,6 +35,7 @@ final class ServerEnvironment { if uri.last == "/" { uri = String(uri.dropLast()) } + LogManager.shared.log.debug("Normalized URI: \"\(uri)\", attempting to getPublicSystemInfo()") JellyfinAPI.basePath = uri return SystemAPI.getPublicSystemInfo() diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index bfbd0467..3e039aa7 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -65,6 +65,7 @@ final class SessionManager { header.append("Device=\"\(deviceName)\", ") if devID == nil { + LogManager.shared.log.info("Generating device ID..."); #if os(tvOS) header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ") deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))" @@ -73,6 +74,7 @@ final class SessionManager { deviceID = "iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))" #endif } else { + LogManager.shared.log.info("Using stored device ID..."); header.append("DeviceId=\"\(devID!)\", ") deviceID = devID! } diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 9f0d6e85..6e24afe5 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -33,12 +33,14 @@ final class HomeViewModel: ViewModel { } func refresh() { + LogManager.shared.log.debug("Refresh called.") UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id!) .trackActivity(loading) .sink(receiveCompletion: { completion in self.handleAPIRequestCompletion(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")") if item.collectionType == "movies" || item.collectionType == "tvshows" { self.libraries.append(item) } @@ -51,6 +53,7 @@ final class HomeViewModel: ViewModel { }, receiveValue: { response in self.libraries.forEach { library in if !(response.configuration?.latestItemsExcludes?.contains(library.id!))! { + LogManager.shared.log.debug("Adding library \(library.id!) (\(library.name ?? "nil")) to recently added list") self.librariesShowRecentlyAddedIDs.append(library.id!) } } @@ -66,6 +69,7 @@ final class HomeViewModel: ViewModel { .sink(receiveCompletion: { completion in self.handleAPIRequestCompletion(completion: completion) }, receiveValue: { response in + LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items") self.resumeItems = response.items ?? [] }) .store(in: &cancellables) @@ -76,6 +80,7 @@ final class HomeViewModel: ViewModel { .sink(receiveCompletion: { completion in self.handleAPIRequestCompletion(completion: completion) }, receiveValue: { response in + LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items") self.nextUpItems = response.items ?? [] }) .store(in: &cancellables) diff --git a/Shared/ViewModels/LatestMediaViewModel.swift b/Shared/ViewModels/LatestMediaViewModel.swift index f17fe3cf..83163c0b 100644 --- a/Shared/ViewModels/LatestMediaViewModel.swift +++ b/Shared/ViewModels/LatestMediaViewModel.swift @@ -25,6 +25,7 @@ final class LatestMediaViewModel: ViewModel { } func requestLatestMedia() { + LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.current.user.user_id ?? "NIL")") UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, parentId: libraryID, fields: [ .primaryImageAspectRatio, @@ -40,6 +41,7 @@ final class LatestMediaViewModel: ViewModel { self?.handleAPIRequestCompletion(completion: completion) }, receiveValue: { [weak self] response in self?.items = response + LogManager.shared.log.debug("Retrieved \(String(self?.items.count ?? 0)) items") }) .store(in: &cancellables) } diff --git a/Shared/ViewModels/SeasonItemViewModel.swift b/Shared/ViewModels/SeasonItemViewModel.swift index 90895feb..499e7901 100644 --- a/Shared/ViewModels/SeasonItemViewModel.swift +++ b/Shared/ViewModels/SeasonItemViewModel.swift @@ -22,6 +22,7 @@ final class SeasonItemViewModel: DetailItemViewModel { } func requestEpisodes() { + LogManager.shared.log.debug("Getting episodes in season \(self.item.id!) (\(self.item.name!)) of show \(self.item.seriesId!) (\(self.item.seriesName!))") TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "") @@ -30,6 +31,7 @@ final class SeasonItemViewModel: DetailItemViewModel { self?.handleAPIRequestCompletion(completion: completion) }, receiveValue: { [weak self] response in self?.episodes = response.items ?? [] + LogManager.shared.log.debug("Retrieved \(String(self?.episodes.count ?? 0)) episodes") }) .store(in: &cancellables) } diff --git a/Shared/ViewModels/SeriesItemViewModel.swift b/Shared/ViewModels/SeriesItemViewModel.swift index 11b094a7..7bbf86a1 100644 --- a/Shared/ViewModels/SeriesItemViewModel.swift +++ b/Shared/ViewModels/SeriesItemViewModel.swift @@ -24,6 +24,7 @@ final class SeriesItemViewModel: DetailItemViewModel { } func getNextUp() { + LogManager.shared.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))") 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 @@ -53,12 +54,14 @@ final class SeriesItemViewModel: DetailItemViewModel { } func requestSeasons() { + LogManager.shared.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))") 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) }, receiveValue: { [weak self] response in self?.seasons = response.items ?? [] + LogManager.shared.log.debug("Retrieved \(String(self?.seasons.count ?? 0)) seasons") }) .store(in: &cancellables) } diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index 8ad1033a..48b697fd 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -43,10 +43,10 @@ final class SettingsViewModel: ObservableObject { do { self.bitrates = try JSONDecoder().decode([Bitrates].self, from: jsonData) } catch { - print(error) + LogManager.shared.log.error("Error converting processed JSON into Swift compatible schema.") } } catch { - print(error) + LogManager.shared.log.error("Error processing JSON file `bitrates.json`") } self.langs = Locale.isoLanguageCodes.compactMap { diff --git a/Shared/ViewModels/SplashViewModel.swift b/Shared/ViewModels/SplashViewModel.swift index 57776ab6..5a33abad 100644 --- a/Shared/ViewModels/SplashViewModel.swift +++ b/Shared/ViewModels/SplashViewModel.swift @@ -36,12 +36,12 @@ final class SplashViewModel: ViewModel { } @objc func didLogIn() { - print("didLogIn") + LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.") isLoggedIn = true } @objc func didLogOut() { - print("didLogOut") + LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.") isLoggedIn = false } } diff --git a/Shared/ViewModels/ViewModel.swift b/Shared/ViewModels/ViewModel.swift index 6417aa6e..2f057641 100644 --- a/Shared/ViewModels/ViewModel.swift +++ b/Shared/ViewModels/ViewModel.swift @@ -40,9 +40,12 @@ class ViewModel: ObservableObject { 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 } }