Add related items, add logging statements.
This commit is contained in:
parent
c6a93868c7
commit
f1dc7be31d
|
@ -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" */;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 <acvigue@me.com>"])
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ final class ServerEnvironment {
|
|||
}
|
||||
|
||||
func create(with uri: String) -> AnyPublisher<Server, Error> {
|
||||
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()
|
||||
|
|
|
@ -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!
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue