From 007930ec06275edd87a937fe3631196aefea47ca Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Sun, 6 Jun 2021 21:54:07 -0700 Subject: [PATCH] Start moving to generated client --- JellyfinPlayer.xcodeproj/project.pbxproj | 46 +-- JellyfinPlayer/APIs/JellyApiTypings.swift | 79 ----- JellyfinPlayer/APIs/JellyfinAPIOld.swift | 4 +- JellyfinPlayer/ConnectToServerView.swift | 326 ++++++++---------- JellyfinPlayer/ContentView.swift | 85 ++--- JellyfinPlayer/ContinueWatchingView.swift | 169 +++------ JellyfinPlayer/DeviceProfileBuilder.swift | 144 +++----- JellyfinPlayer/EpisodeItemView.swift | 30 +- JellyfinPlayer/ItemView.swift | 27 +- JellyfinPlayer/JellyfinPlayerApp.swift | 18 - JellyfinPlayer/LatestMediaView.swift | 150 ++------ JellyfinPlayer/LibraryFilterView.swift | 4 +- JellyfinPlayer/LibrarySearchView.swift | 4 +- JellyfinPlayer/LibraryView.swift | 4 +- JellyfinPlayer/MovieItemView.swift | 34 +- JellyfinPlayer/NextUpView.swift | 134 +++---- JellyfinPlayer/SeasonItemView.swift | 26 +- JellyfinPlayer/SeriesItemView.swift | 6 +- JellyfinPlayer/Views/SettingsView.swift | 6 +- JellyfinPlayer/Views/VideoPlayer.swift | 268 ++++++-------- Shared/Extensions/APIExtensions.swift | 81 +++++ ...Selector.swift => MultiSelectorView.swift} | 0 .../{LazyImage.swift => NukeExtensions.swift} | 0 .../{String.swift => StringExtensions.swift} | 17 + .../JellyfinPlayer.xcdatamodel/contents | 14 +- Shared/Typings/Typings.swift | 4 +- 26 files changed, 659 insertions(+), 1021 deletions(-) delete mode 100644 JellyfinPlayer/APIs/JellyApiTypings.swift create mode 100644 Shared/Extensions/APIExtensions.swift rename Shared/Extensions/{MultiSelector.swift => MultiSelectorView.swift} (100%) rename Shared/Extensions/{LazyImage.swift => NukeExtensions.swift} (100%) rename Shared/Extensions/{String.swift => StringExtensions.swift} (62%) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 3a2e40ad..7aef2827 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -34,13 +34,15 @@ 535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; }; 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; - 535870A72669D8AE00D05A09 /* MultiSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelector.swift */; }; - 535870A82669D8AE00D05A09 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* String.swift */; }; - 535870A92669D8AE00D05A09 /* LazyImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* LazyImage.swift */; }; + 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; + 535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; + 535870A92669D8AE00D05A09 /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* NukeExtensions.swift */; }; 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; }; 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; }; 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VideoPlayer.swift */; }; + 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; + 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; }; 5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF6263B596A003A4E83 /* ContentView.swift */; }; 5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; }; @@ -50,7 +52,6 @@ 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; }; 53892770263C25230035E14B /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276F263C25230035E14B /* NextUpView.swift */; }; 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892771263C8C6F0035E14B /* LoadingView.swift */; }; - 53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892776263CBB000035E14B /* JellyApiTypings.swift */; }; 5389277A263CBFE70035E14B /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 53892779263CBFE70035E14B /* SwiftyJSON */; }; 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; }; 53987CA426572C1300E7EA70 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; }; @@ -66,17 +67,17 @@ 53D5E3DE264B47EE00BADDC8 /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; }; 53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; }; - 53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelector.swift */; }; + 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; }; 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */; }; 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; }; 6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388D265F777C00A81A2A /* LibraryViewModel.swift */; }; 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; }; - 621338932660107500A81A2A /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* String.swift */; }; + 621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; 62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62133894266096EF00A81A2A /* LibraryListViewModel.swift */; }; 621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; 621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; }; - 621C638226676728004216EA /* LazyImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* LazyImage.swift */; }; + 621C638226676728004216EA /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* NukeExtensions.swift */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 6273DD43265F4195009C1D0B /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD42265F4195009C1D0B /* Moya */; }; 6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD44265F4195009C1D0B /* CombineMoya */; }; @@ -138,6 +139,7 @@ 535870AC2669D8DD00D05A09 /* Typings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typings.swift; sourceTree = ""; }; 535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; 535BAEA4264A151C005FA86D /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; + 5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = ""; }; 5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JellyfinPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = ""; }; 5377CBF6263B596A003A4E83 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -149,7 +151,6 @@ 5389276D263C25100035E14B /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; 5389276F263C25230035E14B /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = ""; }; 53892771263C8C6F0035E14B /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; - 53892776263CBB000035E14B /* JellyApiTypings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyApiTypings.swift; sourceTree = ""; }; 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; 53987CA326572C1300E7EA70 /* SeasonItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemView.swift; sourceTree = ""; }; 53987CA526572F0700E7EA70 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = ""; }; @@ -161,16 +162,16 @@ 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = ""; }; 53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = ""; }; - 53E4E648263F725B00F67C6B /* MultiSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelector.swift; sourceTree = ""; }; + 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = ""; }; 53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = ""; }; 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = ""; }; 6213388D265F777C00A81A2A /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = ""; }; 6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; - 621338922660107500A81A2A /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + 621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; 62133894266096EF00A81A2A /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; 621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; - 621C638126676728004216EA /* LazyImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyImage.swift; sourceTree = ""; }; + 621C638126676728004216EA /* NukeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeExtensions.swift; sourceTree = ""; }; 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = ""; }; 6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIOld.swift; sourceTree = ""; }; 6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; @@ -331,12 +332,13 @@ isa = PBXGroup; children = ( 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */, - 53E4E648263F725B00F67C6B /* MultiSelector.swift */, + 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */, 621338B22660A07800A81A2A /* LazyView.swift */, - 621338922660107500A81A2A /* String.swift */, + 621338922660107500A81A2A /* StringExtensions.swift */, 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */, - 621C638126676728004216EA /* LazyImage.swift */, + 621C638126676728004216EA /* NukeExtensions.swift */, 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */, + 5364F454266CA0DC0026ECBA /* APIExtensions.swift */, ); path = Extensions; sourceTree = ""; @@ -344,7 +346,6 @@ 6273DD46265F419B009C1D0B /* APIs */ = { isa = PBXGroup; children = ( - 53892776263CBB000035E14B /* JellyApiTypings.swift */, 6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */, ); path = APIs; @@ -529,10 +530,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 535870A82669D8AE00D05A09 /* String.swift in Sources */, + 535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */, 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, - 535870A72669D8AE00D05A09 /* MultiSelector.swift in Sources */, - 535870A92669D8AE00D05A09 /* LazyImage.swift in Sources */, + 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, + 535870A92669D8AE00D05A09 /* NukeExtensions.swift in Sources */, 5358706C2669D21700D05A09 /* Persistence.swift in Sources */, 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, 535870652669D21600D05A09 /* ContentView.swift in Sources */, @@ -540,6 +541,7 @@ 53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */, 5358706F2669D21700D05A09 /* JellyfinPlayer_tvOS.xcdatamodeld in Sources */, 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */, + 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */, 535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -548,8 +550,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 621338932660107500A81A2A /* String.swift in Sources */, - 621C638226676728004216EA /* LazyImage.swift in Sources */, + 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */, + 621338932660107500A81A2A /* StringExtensions.swift in Sources */, + 621C638226676728004216EA /* NukeExtensions.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, @@ -565,7 +568,7 @@ 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */, - 53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */, + 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */, 6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, @@ -573,7 +576,6 @@ 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, 6273DD48265F41B3009C1D0B /* JellyfinAPIOld.swift in Sources */, 53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */, - 53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */, 5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */, 53987CA82657424A00E7EA70 /* EpisodeItemView.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, diff --git a/JellyfinPlayer/APIs/JellyApiTypings.swift b/JellyfinPlayer/APIs/JellyApiTypings.swift deleted file mode 100644 index 42e251bc..00000000 --- a/JellyfinPlayer/APIs/JellyApiTypings.swift +++ /dev/null @@ -1,79 +0,0 @@ -/* JellyfinPlayer/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 SwiftUI - -extension View { - func rectReader(_ binding: Binding, in space: CoordinateSpace) -> some View { - self.background(GeometryReader { (geometry) -> AnyView in - let rect = geometry.frame(in: space) - DispatchQueue.main.async { - binding.wrappedValue = rect - } - return AnyView(Rectangle().fill(Color.clear)) - }) - } -} - -extension View { - func ifVisible(in rect: CGRect, in space: CoordinateSpace, execute: @escaping (CGRect) -> Void) -> some View { - self.background(GeometryReader { (geometry) -> AnyView in - let frame = geometry.frame(in: space) - if frame.intersects(rect) { - execute(frame) - } - return AnyView(Rectangle().fill(Color.clear)) - }) - } -} - -struct ServerPublicInfoResponse: Codable { - var LocalAddress: String - var ServerName: String - var Version: String - var ProductName: String - var OperatingSystem: String - var Id: String - var StartupWizardCompleted: Bool -} - -struct ServerUserResponse: Codable { - var Name: String - var Id: String - var PrimaryImageTag: String -} - -struct ServerAuthByNameResponse: Codable { - var User: ServerUserResponse - var AccessToken: String -} - -struct ResumeItem { - var Name: String = ""; - var Id: String = ""; - var IndexNumber: Int? = nil; - var ParentIndexNumber: Int? = nil; - var Image: String = ""; - var ImageType: String = ""; - var BlurHash: String = ""; - var `Type`: String = ""; - var SeasonId: String? = nil; - var SeriesId: String? = nil; - var SeriesName: String? = nil; - var ItemProgress: Double = 0; - var SeasonImage: String? = nil; - var SeasonImageType: String? = nil; - var SeasonImageBlurHash: String? = nil; - var ItemBadge: Int? = 0; - var ProductionYear: Int = 1999; - var Watched: Bool = false; -} - -struct ServerMeResponse: Codable { - -} diff --git a/JellyfinPlayer/APIs/JellyfinAPIOld.swift b/JellyfinPlayer/APIs/JellyfinAPIOld.swift index 258824de..1c8fa433 100644 --- a/JellyfinPlayer/APIs/JellyfinAPIOld.swift +++ b/JellyfinPlayer/APIs/JellyfinAPIOld.swift @@ -88,7 +88,7 @@ extension JellyfinAPIOld: TargetType { switch self { case let .items(global, _, _), let .search(global, _, _, _): - return URL(string: global.server?.baseURI ?? "")! + return URL(string: global.server.baseURI ?? "")! } } @@ -96,7 +96,7 @@ extension JellyfinAPIOld: TargetType { switch self { case let .items(global, _, _), let .search(global, _, _, _): - return "/Users/\(global.user?.user_id ?? "")/Items" + return "/Users/\(global.user.user_id ?? "")/Items" } } diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 1fe8f440..ab7dcd5e 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -5,26 +5,19 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ -import SwiftUI +//MARK: refactor this file! it's the first swift file I ever wrote and it clearly shows. -import SwiftyRequest -import SwiftyJSON +import SwiftUI import CoreData import KeychainSwift import NukeUI import JellyfinAPI -class publicUser: ObservableObject { - @Published var username: String = ""; - @Published var hasPassword: Bool = true; - @Published var primaryImageTag: String = ""; - @Published var id: String = ""; -} - struct ConnectToServerView: View { @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject var globalData: GlobalData @EnvironmentObject var jsi: justSignedIn + @State private var uri = ""; @State private var isWorking = false; @State private var isErrored = false; @@ -33,27 +26,26 @@ struct ConnectToServerView: View { @State private var isConnected = false; @State private var serverName = ""; @State private var usernameDisabled: Bool = false; - @State private var publicUsers: [publicUser] = []; - @State private var lastPublicUsers: [publicUser] = []; - @Binding var rootIsActive : Bool - - let userUUID = UUID(); - + @State private var publicUsers: [UserDto] = []; + @State private var lastPublicUsers: [UserDto] = []; @State private var username = ""; @State private var password = ""; @State private var server_id = ""; - @State private var serverSkipped: Bool = false; @State private var serverSkippedAlert: Bool = false; - private var reauthDeviceID: String = ""; - private var skip_server_bool: Bool = false; - private var skip_server_obj: Server? + @State private var skip_server_bool: Bool = false; + @State private var skip_server_obj: Server = Server(); - init(skip_server: Bool, skip_server_prefill: Server?, reauth_deviceId: String, isActive: Binding) { + @Binding var rootIsActive: Bool + + private var reauthDeviceID: String = ""; + private let userUUID = UUID(); + + init(skip_server: Bool, skip_server_prefill: Server, reauth_deviceId: String, isActive: Binding) { + _rootIsActive = isActive skip_server_bool = skip_server skip_server_obj = skip_server_prefill reauthDeviceID = reauth_deviceId - _rootIsActive = isActive } init(isActive: Binding) { @@ -62,111 +54,91 @@ struct ConnectToServerView: View { func start() { if(skip_server_bool) { - _uri.wrappedValue = skip_server_obj?.baseURI ?? "" - let request = RestRequest(method: .get, url: uri + "/users/public") - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let response): - do { - let body = response.body; - let json = try JSON(data: body); - - for (_,publicUserDto):(String, JSON) in json { - let newPublicUser = publicUser() - newPublicUser.username = publicUserDto["Name"].string ?? "" - newPublicUser.hasPassword = publicUserDto["HasPassword"].bool ?? true - newPublicUser.primaryImageTag = publicUserDto["PrimaryImageTag"].string ?? "" - newPublicUser.id = publicUserDto["Id"].string ?? "" - _publicUsers.wrappedValue.append(newPublicUser) - } - } catch(_) { - + uri = skip_server_obj.baseURI! + + UserAPI.getPublicUsers() + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + break + case .failure(_): + skip_server_bool = false; + skip_server_obj = Server(); + break } - _serverSkipped.wrappedValue = true; - _serverSkippedAlert.wrappedValue = true; - _server_id.wrappedValue = skip_server_obj?.server_id ?? "" - _serverName.wrappedValue = skip_server_obj?.name ?? "" - _isConnected.wrappedValue = true; - break - case .failure(_): - _serverSkipped.wrappedValue = true; - _serverSkippedAlert.wrappedValue = true; - _server_id.wrappedValue = skip_server_obj?.server_id ?? "" - _serverName.wrappedValue = skip_server_obj?.name ?? "" - _isConnected.wrappedValue = true; - break - } - - } + }, receiveValue: { response in + publicUsers = response + + serverSkipped = true; + serverSkippedAlert = true; + server_id = skip_server_obj.server_id! + serverName = skip_server_obj.name! + isConnected = true; + }) + .store(in: &globalData.pendingAPIRequests) } } func doLogin() { - _isWorking.wrappedValue = true - let authJson: [String: Any] = ["Username": _username.wrappedValue, "Pw": _password.wrappedValue] - let request = RestRequest(method: .post, url: uri + "/Users/authenticatebyname") + isWorking = true let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String; - let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(UIDevice.current.name)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\""; + var deviceName = UIDevice.current.name; + deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current) + deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]"); - request.headerParameters["X-Emby-Authorization"] = authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBodyDictionary = authJson + let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\""; - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let response): + JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader + + UserAPI.authenticateUser(userId: username, pw: password) + .sink(receiveCompletion: { completion in + isWorking = false + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { response in + let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Server") + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + do { - let json = try JSON(data: response.body) - let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Server") - let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + try viewContext.execute(deleteRequest) + } catch _ as NSError { - do { - try viewContext.execute(deleteRequest) - } catch _ as NSError { - // TODO: handle the error - } - - let fetchRequest2: NSFetchRequest = NSFetchRequest(entityName: "SignedInUser") - let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2) + } + + let fetchRequest2: NSFetchRequest = NSFetchRequest(entityName: "SignedInUser") + let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2) - do { - try viewContext.execute(deleteRequest2) - } catch _ as NSError { - // TODO: handle the error - } - - let newServer = Server(context: viewContext) - newServer.baseURI = _uri.wrappedValue - newServer.name = _serverName.wrappedValue - newServer.server_id = _server_id.wrappedValue - - let newUser = SignedInUser(context: viewContext) - newUser.device_uuid = userUUID.uuidString - newUser.username = _username.wrappedValue - newUser.user_id = json["User"]["Id"].string ?? "" - - let keychain = KeychainSwift() - keychain.set(json["AccessToken"].string ?? "", forKey: "AccessToken_\(json["User"]["Id"].string ?? "")") - - do { - try viewContext.save() - DispatchQueue.main.async { [self] in - globalData.authHeader = authHeader - _rootIsActive.wrappedValue = false - jsi.did = true - } - } catch { - } - } catch { + do { + try viewContext.execute(deleteRequest2) + } catch _ as NSError { } - case .failure(_): - _isSignInErrored.wrappedValue = true; - } - _isWorking.wrappedValue = false; - } + + let newServer = Server(context: viewContext) + newServer.baseURI = uri + newServer.name = serverName + newServer.server_id = server_id + + let newUser = SignedInUser(context: viewContext) + newUser.device_uuid = userUUID.uuidString + newUser.username = username + newUser.user_id = response.user!.id! + + let keychain = KeychainSwift() + keychain.set(response.accessToken!, forKey: "AccessToken_\(newUser.user_id!)") + + do { + try viewContext.save() + DispatchQueue.main.async { [self] in + globalData.authHeader = authHeader + _rootIsActive.wrappedValue = false + jsi.did = true + } + } catch { + print("Couldn't store objects to CoreData") + } + }) + .store(in: &globalData.pendingAPIRequests) } var body: some View { @@ -177,56 +149,50 @@ struct ConnectToServerView: View { .disableAutocorrection(true) .autocapitalization(.none) Button { - _isWorking.wrappedValue = true; - if(!_uri.wrappedValue.contains("http")) { - _uri.wrappedValue = "http://" + _uri.wrappedValue; + isWorking = true; + if(!uri.contains("http")) { + uri = "http://" + uri; } - if(_uri.wrappedValue.last == "/") { - _uri.wrappedValue = String(_uri.wrappedValue.dropLast()) + if(uri.last == "/") { + uri = String(uri.dropLast()) } - let request = RestRequest(method: .get, url: uri + "/System/Info/Public") - request.responseObject() { (result: Result, RestError>) in - switch result { - case .success(let response): - let server = response.body - _serverName.wrappedValue = server.ServerName - _server_id.wrappedValue = server.Id - if(server.StartupWizardCompleted) { - _isConnected.wrappedValue = true; - } - - let request2 = RestRequest(method: .get, url: uri + "/users/public") - request2.responseData() { (result: Result, RestError>) in - switch result { - case .success(let response): - do { - let body = response.body; - let json = try JSON(data: body); - - for (_,publicUserDto):(String, JSON) in json { - let newPublicUser = publicUser() - newPublicUser.username = publicUserDto["Name"].string ?? "" - newPublicUser.hasPassword = publicUserDto["HasPassword"].bool ?? true - newPublicUser.primaryImageTag = publicUserDto["PrimaryImageTag"].string ?? "" - newPublicUser.id = publicUserDto["Id"].string ?? "" - _publicUsers.wrappedValue.append(newPublicUser) - } - } catch(_) { - - } - _isWorking.wrappedValue = false; + + JellyfinAPI.basePath = uri + SystemAPI.getPublicSystemInfo() + .sink(receiveCompletion: { completion in + switch completion { + case .finished: break case .failure(_): - _isErrored.wrappedValue = true; - _isWorking.wrappedValue = false; + isErrored = true + isWorking = false break - } } - case .failure(_): - _isErrored.wrappedValue = true; - _isWorking.wrappedValue = false; - } - } + }, receiveValue: { response in + let server = response + serverName = server.serverName! + server_id = server.id! + if(server.startupWizardCompleted!) { + isConnected = true; + + UserAPI.getPublicUsers() + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + break + case .failure(_): + isErrored = true + isWorking = false + break + } + }, receiveValue: { response in + publicUsers = response + isWorking = false + }) + .store(in: &globalData.pendingAPIRequests) + } + }) + .store(in: &globalData.pendingAPIRequests) } label: { HStack { Text("Connect") @@ -240,7 +206,7 @@ struct ConnectToServerView: View { Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again"))) } } else { - if(_publicUsers.wrappedValue.count == 0) { + if(publicUsers.count == 0) { Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) { TextField("Username", text: $username) .disableAutocorrection(true) @@ -268,11 +234,11 @@ struct ConnectToServerView: View { if(serverSkipped) { Section() { Button { - _serverSkippedAlert.wrappedValue = false; - _server_id.wrappedValue = "" - _serverName.wrappedValue = "" - _isConnected.wrappedValue = false; - _serverSkipped.wrappedValue = false; + serverSkippedAlert = false + server_id = "" + serverName = "" + isConnected = false + serverSkipped = false } label: { HStack() { HStack() { @@ -286,8 +252,8 @@ struct ConnectToServerView: View { } else { Section() { Button { - _publicUsers.wrappedValue = _lastPublicUsers.wrappedValue - _usernameDisabled.wrappedValue = false; + publicUsers = lastPublicUsers + usernameDisabled = false; } label: { HStack() { HStack() { @@ -301,26 +267,26 @@ struct ConnectToServerView: View { } } else { Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) { - ForEach(publicUsers, id: \.id) { pubuser in + ForEach(publicUsers, id: \.id) { publicUser in HStack() { Button() { - if(pubuser.hasPassword) { - _lastPublicUsers.wrappedValue = _publicUsers.wrappedValue - _username.wrappedValue = pubuser.username - _usernameDisabled.wrappedValue = true; - _publicUsers.wrappedValue = [] + if(publicUser.hasPassword!) { + lastPublicUsers = publicUsers + username = publicUser.name! + usernameDisabled = true + publicUsers = [] } else { - _publicUsers.wrappedValue = [] - _password.wrappedValue = ""; - _username.wrappedValue = pubuser.username + publicUsers = [] + password = "" + username = publicUser.name! doLogin() } } label: { HStack() { - Text(pubuser.username).font(.subheadline).fontWeight(.semibold) + Text(publicUser.name!).font(.subheadline).fontWeight(.semibold) Spacer() - if(pubuser.primaryImageTag != "") { - LazyImage(source: URL(string: "\(uri)/Users/\(pubuser.id)/Images/Primary?width=200&quality=80&tag=\(pubuser.primaryImageTag)")) + if(publicUser.primaryImageTag != "") { + LazyImage(source: URL(string: "\(uri)/Users/\(publicUser.id!)/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)")) .contentMode(.aspectFill) .frame(width: 60, height: 60) .cornerRadius(30.0) @@ -342,9 +308,9 @@ struct ConnectToServerView: View { Section() { Button() { - _lastPublicUsers.wrappedValue = _publicUsers.wrappedValue; - _publicUsers.wrappedValue = [] - _username.wrappedValue = "" + lastPublicUsers = publicUsers + publicUsers = [] + username = "" } label: { HStack() { Text("Other User").font(.subheadline).fontWeight(.semibold) diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift index 39d16f0e..8c5258f3 100644 --- a/JellyfinPlayer/ContentView.swift +++ b/JellyfinPlayer/ContentView.swift @@ -8,51 +8,40 @@ import SwiftUI import KeychainSwift -import SwiftyJSON -import SwiftyRequest import Nuke import Combine import JellyfinAPI struct ContentView: View { - @Environment(\.managedObjectContext) - private var viewContext - @EnvironmentObject - var orientationInfo: OrientationInfo - @StateObject - private var globalData = GlobalData() - @EnvironmentObject - var jsi: justSignedIn + @Environment(\.managedObjectContext) private var viewContext + @EnvironmentObject var orientationInfo: OrientationInfo + @EnvironmentObject var jsi: justSignedIn + + @StateObject private var globalData = GlobalData() - @FetchRequest(entity: Server.entity(), - sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)]) - private var servers: FetchedResults + @FetchRequest(entity: Server.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)]) + private var servers: FetchedResults - @FetchRequest(entity: SignedInUser.entity(), - sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username, - ascending: true)]) - private var savedUsers: FetchedResults + @FetchRequest(entity: SignedInUser.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username, ascending: true)]) + private var savedUsers: FetchedResults - @State - private var needsToSelectServer = false - @State - private var isLoading = false - @State - private var tabSelection: String = "Home" - @State - private var libraries: [String] = [] - @State - private var library_names: [String: String] = [:] - @State - private var librariesShowRecentlyAdded: [String] = [] - @State - private var libraryPrefillID: String = "" - @State - private var showSettingsPopover: Bool = false - @State - private var viewDidLoad: Bool = false + @State private var needsToSelectServer = false + @State private var isLoading = false + @State private var tabSelection: String = "Home" + @State private var libraries: [String] = [] + @State private var library_names: [String: String] = [:] + @State private var librariesShowRecentlyAdded: [String] = [] + @State private var libraryPrefillID: String = "" + @State private var showSettingsPopover: Bool = false + @State private var viewDidLoad: Bool = false func startup() { + if(viewDidLoad == true) { + return + } + + viewDidLoad = true + let size = UIScreen.main.bounds.size if size.width < size.height { orientationInfo.orientation = .portrait @@ -60,12 +49,6 @@ struct ContentView: View { orientationInfo.orientation = .landscape } - if viewDidLoad { - return - } - - viewDidLoad = true - ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk @@ -91,19 +74,18 @@ struct ContentView: View { var header = "MediaBrowser " header.append("Client=\"SwiftFin\", ") header.append("Device=\"\(deviceName)\", ") - header.append("DeviceId=\"\(globalData.user?.device_uuid ?? "")\", ") + header.append("DeviceId=\"\(globalData.user.device_uuid ?? "")\", ") header.append("Version=\"\(appVersion ?? "0.0.1")\", ") header.append("Token=\"\(globalData.authToken)\"") globalData.authHeader = header - JellyfinAPI.basePath = globalData.server?.baseURI ?? "" + JellyfinAPI.basePath = globalData.server.baseURI ?? "" JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader] UserAPI.getCurrentUser() .sink(receiveCompletion: { completion in HandleAPIRequestCompletion(globalData: globalData, completion: completion) }, receiveValue: { response in - //Get all libraries libraries = response.configuration?.orderedViews ?? [] librariesShowRecentlyAdded = libraries.filter { element in return !(response.configuration?.latestItemsExcludes?.contains(element))! @@ -111,11 +93,10 @@ struct ContentView: View { }) .store(in: &globalData.pendingAPIRequests) - UserViewsAPI.getUserViews(userId: globalData.user?.user_id ?? "") + UserViewsAPI.getUserViews(userId: globalData.user.user_id ?? "") .sink(receiveCompletion: { completion in HandleAPIRequestCompletion(globalData: globalData, completion: completion) }, receiveValue: { response in - //Get all libraries response.items?.forEach({ item in library_names[item.id ?? ""] = item.name }) @@ -144,7 +125,7 @@ struct ContentView: View { } else if (globalData.expiredCredentials == true) { NavigationView { ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server, - reauth_deviceId: globalData.user?.device_uuid ?? "", isActive: $globalData.expiredCredentials) + reauth_deviceId: globalData.user.device_uuid ?? "", isActive: $globalData.expiredCredentials) } .navigationViewStyle(StackNavigationViewStyle()) .environmentObject(globalData) @@ -155,9 +136,9 @@ struct ContentView: View { NavigationView { VStack(alignment: .leading) { ScrollView { - Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 15) + Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 16) ContinueWatchingView() - NextUpView().padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) + NextUpView() ForEach(librariesShowRecentlyAdded, id: \.self) { library_id in VStack(alignment: .leading) { HStack { @@ -171,7 +152,7 @@ struct ContentView: View { Text("See All").font(.subheadline).fontWeight(.bold) } }.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - LatestMediaView(library: library_id) + LatestMediaView(usingLibraryID: library_id) }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) } Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) @@ -211,10 +192,10 @@ struct ContentView: View { .environmentObject(globalData) .onAppear(perform: startup) .alert(isPresented: $globalData.networkError) { - Alert(title: Text("Network Error"), message: Text("Couldn't connect to Jellyfin"), dismissButton: .default(Text("Ok"))) + Alert(title: Text("Network Error"), message: Text("An error occured while performing a network request"), dismissButton: .default(Text("Ok"))) } } else { - Text("Signing in...") + Text("Please wait...") .onAppear(perform: { DispatchQueue.main.async { [self] in _viewDidLoad.wrappedValue = false diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index d9100f54..431fde2b 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -6,27 +6,24 @@ */ import SwiftUI -import SwiftyRequest -import SwiftyJSON import NukeUI +import JellyfinAPI -struct CustomShape: Shape { - let radius: CGFloat - +struct ProgressBar: Shape { func path(in rect: CGRect) -> Path { var path = Path() let tl = CGPoint(x: rect.minX, y: rect.minY) let tr = CGPoint(x: rect.maxX, y: rect.minY) let br = CGPoint(x: rect.maxX, y: rect.maxY) - let bls = CGPoint(x: rect.minX + radius, y: rect.maxY) - let blc = CGPoint(x: rect.minX + radius, y: rect.maxY - radius) + let bls = CGPoint(x: rect.minX + 10, y: rect.maxY) + let blc = CGPoint(x: rect.minX + 10, y: rect.maxY - 10) path.move(to: tl) path.addLine(to: tr) path.addLine(to: br) path.addLine(to: bls) - path.addRelativeArc(center: blc, radius: radius, + path.addRelativeArc(center: blc, radius: 10, startAngle: Angle.degrees(90), delta: Angle.degrees(90)) return path @@ -35,147 +32,77 @@ struct CustomShape: Shape { struct ContinueWatchingView: View { - @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject var globalData: GlobalData - @EnvironmentObject var orientationInfo: OrientationInfo - @State var resumeItems: [ResumeItem] = [] - @State private var viewDidLoad: Int = 0; - @State private var isLoading: Bool = true; + @State private var items: [BaseItemDto] = [] func onAppear() { - if(globalData.server?.baseURI == "") { - return - } - if(viewDidLoad == 1) { - return - } - _viewDidLoad.wrappedValue = 1; - let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/Items/Resume?Limit=12&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&MediaTypes=Video") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let response): - let body = response.body - do { - let json = try JSON(data: body) - for (_,item):(String, JSON) in json["Items"] { - // Do something you want - var itemObj = ResumeItem() - if(item["PrimaryImageAspectRatio"].double ?? 0.0 < 1.0) { - //portrait; use backdrop instead - itemObj.Image = item["BackdropImageTags"][0].string ?? "" - itemObj.ImageType = "Backdrop" - - if(itemObj.Image == "") { - itemObj.Image = item["ParentBackdropImageTags"][0].string ?? "" - } - - itemObj.BlurHash = item["ImageBlurHashes"]["Backdrop"][itemObj.Image].string ?? "" - } else { - itemObj.Image = item["ImageTags"]["Primary"].string ?? "" - itemObj.ImageType = "Primary" - itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" - } - - itemObj.Name = item["Name"].string ?? "" - itemObj.Type = item["Type"].string ?? "" - itemObj.IndexNumber = item["IndexNumber"].int ?? nil - itemObj.Id = item["Id"].string ?? "" - itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil - itemObj.SeasonId = item["SeasonId"].string ?? nil - itemObj.SeriesId = item["SeriesId"].string ?? nil - itemObj.SeriesName = item["SeriesName"].string ?? nil - itemObj.ItemProgress = item["UserData"]["PlayedPercentage"].double ?? 0.00 - _resumeItems.wrappedValue.append(itemObj) - } - _isLoading.wrappedValue = false; - } catch { - - } - break - case .failure(let error): - _viewDidLoad.wrappedValue = 0; - debugPrint(error) - break - } - } + ItemsAPI.getResumeItems(userId: globalData.user.user_id ?? "", limit: 12, fields: [.primaryImageAspectRatio], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary,.backdrop,.thumb]) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { response in + items = response.items ?? [] + }) + .store(in: &globalData.pendingAPIRequests) } var body: some View { ScrollView(.horizontal, showsIndicators: false) { - if(_resumeItems.wrappedValue.count > 0) { + if(items.count > 0) { LazyHStack() { - Spacer().frame(width:12) - ForEach(resumeItems, id: \.Id) { item in - NavigationLink(destination: ItemView(item: item)) { + Spacer().frame(width:14) + ForEach(items, id: \.id) { item in + NavigationLink(destination: EmptyView()) { VStack(alignment: .leading) { Spacer().frame(height: 10) - if(item.Type == "Episode") { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)")) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 48, height: 32))!) - .resizable() - .frame(width: 320, height: 180) - .cornerRadius(10) - } - .frame(width: 320, height: 180) - .cornerRadius(10) - .overlay( - ZStack { - Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0)) - \(item.Name)") + LazyImage(source: item.getBackdropImage(baseURL: globalData.server.baseURI ?? "", maxWidth: 320)) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: item.getBackdropImageBlurHash(), size: CGSize(width: 48, height: 32))!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 320, height: 180) + .cornerRadius(10) + } + .aspectRatio(contentMode: .fill) + .frame(width: 320, height: 180) + .cornerRadius(10) + .overlay( + Group { + if(item.type == "Episode") { + Text("\(item.name!)") .font(.caption) .padding(6) .foregroundColor(.white) - }.background(Color.black) - .opacity(0.8) - .cornerRadius(10.0) - .padding(6), alignment: .topTrailing - ) - .overlay( - Rectangle() - .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .mask(CustomShape(radius: 10)) - .frame(width: CGFloat((item.ItemProgress/100)*320), height: 7) - .padding(0), alignment: .bottomLeading - ) - } else { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)")) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 48, height: 32))!) - .resizable() - .frame(width: 320, height: 180) - .cornerRadius(10) - } - .frame(width: 320, height: 180) - .cornerRadius(10) - .overlay( - Rectangle() - .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .mask(CustomShape(radius: 10)) - .frame(width: CGFloat((item.ItemProgress/100)*320), height: 7) + } + }.background(Color.black) + .opacity(0.8) + .cornerRadius(10.0) + .padding(6), alignment: .topTrailing + ) + .overlay( + Rectangle() + .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) + .mask(ProgressBar()) + .frame(width: CGFloat(item.userData!.playedPercentage!*3.2), height: 7) .padding(0), alignment: .bottomLeading - ) - } - Text("\(item.Type == "Episode" ? item.SeriesName ?? "" : item.Name)") + ) + Text(item.seriesName ?? item.name ?? "") .font(.callout) .fontWeight(.semibold) .foregroundColor(.primary) .lineLimit(1) .frame(width: 320, alignment: .leading) Spacer().frame(height: 5) - }.padding(.trailing, 5) + } } + Spacer().frame(width: 16) } Spacer().frame(width: 2) }.frame(height: 215) + .padding(.bottom, 10) } else { EmptyView() } }.onAppear(perform: onAppear) - .padding(.bottom, 10) } } diff --git a/JellyfinPlayer/DeviceProfileBuilder.swift b/JellyfinPlayer/DeviceProfileBuilder.swift index 09000619..c09839bc 100644 --- a/JellyfinPlayer/DeviceProfileBuilder.swift +++ b/JellyfinPlayer/DeviceProfileBuilder.swift @@ -8,7 +8,7 @@ //lol can someone buy me a coffee this took forever :| import Foundation -import SwiftyJSON +import JellyfinAPI enum CPUModel { case A4 @@ -33,65 +33,6 @@ enum CPUModel { case A99 } -struct _AVDirectProfile: Codable { - var Container: String; - var `Type`: String; - var AudioCodec: String = ""; - var VideoCodec: String = ""; -} - -struct _AVTranscodingProfile: Codable { - var Container: String; - var `Type`: String; - var AudioCodec: String = ""; - var VideoCodec: String = ""; - var Context: String = ""; - var `Protocol`: String = "hls"; - var MaxAudioChannels: String = "6"; - var MinSegments: String = "2"; - var BreakOnNonKeyFrames: Bool = true; -} - -struct _AVCodecCondition: Codable { - var Condition: String; - var Property: String; - var Value: String; - var IsRequired: Bool; -} - -struct _AVCodecProfile: Codable { - var `Type`: String; - var Codec: String = ""; - var Conditions: [_AVCodecCondition] = []; -} - -struct _AVSubtitleProfile: Codable { - var Format: String; - var Method: String; -} - -struct _AVResponseProfile: Codable { - var `Type`: String; - var Container: String; - var MimeType: String; -} - -struct DeviceProfile: Codable { - var MaxStreamingBitrate: Int; - var MaxStaticBitrate: Int; - var MusicStreamingTranscodingBitrate: Int; - var DirectPlayProfiles: [_AVDirectProfile] = []; - var TranscodingProfiles: [_AVTranscodingProfile] = []; - var ContainerProfiles: [_AVDirectProfile] = []; - var CodecProfiles: [_AVCodecProfile] = []; - var SubtitleProfiles: [_AVSubtitleProfile] = []; - var ResponseProfiles: [_AVResponseProfile] = []; -} - -struct DeviceProfileRoot: Codable { - var DeviceProfile: DeviceProfile; -} - class DeviceProfileBuilder { public var bitrate: Int = 0; @@ -99,92 +40,91 @@ class DeviceProfileBuilder { self.bitrate = bitrate } - public func buildProfile() -> DeviceProfileRoot { - print(CPUinfo()) - let MaxStreamingBitrate = bitrate; - let MaxStaticBitrate = bitrate; - let MusicStreamingTranscodingBitrate = 384000; + public func buildProfile() -> DeviceProfile { + let maxStreamingBitrate = bitrate; + let maxStaticBitrate = bitrate; + let musicStreamingTranscodingBitrate = 384000; //Build direct play profiles - var DirectPlayProfiles: [_AVDirectProfile] = []; - DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav", VideoCodec: "h264")] + var directPlayProfiles: [DirectPlayProfile] = []; + directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav", videoCodec: "h264", type: .video)] //Device supports Dolby Digital (AC3, EAC3) if(supportsFeature(minimumSupported: .A8X)) { if(supportsFeature(minimumSupported: .A10)) { - DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", VideoCodec: "hevc,h264,hev1")] //HEVC/H.264 with Dolby Digital + directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "hevc,h264,hev1", type: .video)] //HEVC/H.264 with Dolby Digital } else { - DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "ac3,eac3,aac,mp3,wav,opus", VideoCodec: "h264")] //H.264 with Dolby Digital + directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "ac3,eac3,aac,mp3,wav,opus", videoCodec: "h264", type: .video)] //H.264 with Dolby Digital } } //Device supports Dolby Vision? if(supportsFeature(minimumSupported: .A10X)) { - DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", VideoCodec: "dvhe,dvh1,dva1,dvav,h264,hevc,hev1")] //H.264/HEVC with Dolby Digital - No Atmos - Vision + directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] //H.264/HEVC with Dolby Digital - No Atmos - Vision } //Device supports Dolby Atmos? if(supportsFeature(minimumSupported: .A12)) { - DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus", VideoCodec: "h264,hevc,dvhe,dvh1,dva1,dvav,h264,hevc,hev1")] //H.264/HEVC with Dolby Digital & Atmos - Vision + directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus", videoCodec: "h264,hevc,dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] //H.264/HEVC with Dolby Digital & Atmos - Vision } //Build transcoding profiles - var TranscodingProfiles: [_AVTranscodingProfile] = []; - TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav", VideoCodec: "h264", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "2", MinSegments: "2", BreakOnNonKeyFrames: true)] + var transcodingProfiles: [TranscodingProfile] = []; + transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264", audioCodec: "aac,mp3,wav")] //Device supports Dolby Digital (AC3, EAC3) if(supportsFeature(minimumSupported: .A8X)) { if(supportsFeature(minimumSupported: .A10)) { - TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,eac3,ac3,flac,opus", VideoCodec: "h264,hevc,hev1", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "6", MinSegments: "2", BreakOnNonKeyFrames: true)] + transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "aac,mp3,wav,eac3,ac3,flac,opus", audioCodec: "h264,hevc,hev1", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)] } else { - TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,eac3,ac3,opus", VideoCodec: "h264", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "2", MinSegments: "2", BreakOnNonKeyFrames: true)] + transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264", audioCodec: "aac,mp3,wav,eac3,ac3,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)] } } //Device supports Dolby Vision? if(supportsFeature(minimumSupported: .A10X)) { - TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", VideoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "6", MinSegments: "2", BreakOnNonKeyFrames: true)] + transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)] } //Device supports Dolby Atmos? if(supportsFeature(minimumSupported: .A12)) { - TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,dts,truehd,dca,opus", VideoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "9", MinSegments: "2", BreakOnNonKeyFrames: true)] + transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", audioCodec: "aac,mp3,wav,ac3,eac3,flac,dts,truehd,dca,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)] } - var CodecProfiles: [_AVCodecProfile] = [] + var codecProfiles: [CodecProfile] = [] - let h264CodecConditions: [_AVCodecCondition] = [ - _AVCodecCondition(Condition: "NotEquals", Property: "IsAnamorphic", Value: "true", IsRequired: false), - _AVCodecCondition(Condition: "EqualsAny", Property: "VideoProfile", Value: "high|main|baseline|constrained baseline", IsRequired: false), - _AVCodecCondition(Condition: "LessThanEqual", Property: "VideoLevel", Value: "60", IsRequired: false), - _AVCodecCondition(Condition: "NotEquals", Property: "IsInterlaced", Value: "true", IsRequired: false)] - let hevcCodecConditions: [_AVCodecCondition] = [ - _AVCodecCondition(Condition: "NotEquals", Property: "IsAnamorphic", Value: "true", IsRequired: false), - _AVCodecCondition(Condition: "EqualsAny", Property: "VideoProfile", Value: "main|main 10", IsRequired: false), - _AVCodecCondition(Condition: "LessThanEqual", Property: "VideoLevel", Value: "160", IsRequired: false), - _AVCodecCondition(Condition: "NotEquals", Property: "IsInterlaced", Value: "true", IsRequired: false)] + let h264CodecConditions: [ProfileCondition] = [ + ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false), + ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|baseline|constrained baseline", isRequired: false), + ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "60", isRequired: false), + ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)] + let hevcCodecConditions: [ProfileCondition] = [ + ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false), + ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "main|main 10", isRequired: false), + ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "160", isRequired: false), + ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)] - CodecProfiles.append(_AVCodecProfile(Type: "Video", Codec: "h264", Conditions: h264CodecConditions)) + codecProfiles.append(CodecProfile(type: .video, applyConditions: h264CodecConditions, codec: "h264")) if(supportsFeature(minimumSupported: .A10)) { - CodecProfiles.append(_AVCodecProfile(Type: "Video", Codec: "hevc", Conditions: hevcCodecConditions)) + codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions,codec: "hevc")) } - var SubtitleProfiles: [_AVSubtitleProfile] = [] - SubtitleProfiles.append(_AVSubtitleProfile(Format: "vtt", Method: "External")) - SubtitleProfiles.append(_AVSubtitleProfile(Format: "ass", Method: "External")) - SubtitleProfiles.append(_AVSubtitleProfile(Format: "ssa", Method: "External")) - SubtitleProfiles.append(_AVSubtitleProfile(Format: "pgssub", Method: "Embed")) - SubtitleProfiles.append(_AVSubtitleProfile(Format: "sub", Method: "Embed")) - SubtitleProfiles.append(_AVSubtitleProfile(Format: "rip", Method: "Embed")) - SubtitleProfiles.append(_AVSubtitleProfile(Format: "srt", Method: "Embed")) - SubtitleProfiles.append(_AVSubtitleProfile(Format: "pgs", Method: "Embed")) + var subtitleProfiles: [SubtitleProfile] = [] + subtitleProfiles.append(SubtitleProfile(format: "vtt", method: .external)) + subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external)) + subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external)) + subtitleProfiles.append(SubtitleProfile(format: "pgssub", method: .embed)) + subtitleProfiles.append(SubtitleProfile(format: "sub", method: .embed)) + subtitleProfiles.append(SubtitleProfile(format: "rip", method: .embed)) + subtitleProfiles.append(SubtitleProfile(format: "srt", method: .embed)) + subtitleProfiles.append(SubtitleProfile(format: "pgs", method: .embed)) - let ResponseProfiles: [_AVResponseProfile] = [_AVResponseProfile(Type: "Video", Container: "m4v", MimeType: "video/mp4")] + let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", type: .video, mimeType: "video/mp4")] - let DP = DeviceProfile(MaxStreamingBitrate: MaxStreamingBitrate, MaxStaticBitrate: MaxStaticBitrate, MusicStreamingTranscodingBitrate: MusicStreamingTranscodingBitrate, DirectPlayProfiles: DirectPlayProfiles, TranscodingProfiles: TranscodingProfiles, CodecProfiles: CodecProfiles, SubtitleProfiles: SubtitleProfiles, ResponseProfiles: ResponseProfiles) + let profile = DeviceProfile(maxStreamingBitrate: maxStreamingBitrate, maxStaticBitrate: maxStaticBitrate, musicStreamingTranscodingBitrate: musicStreamingTranscodingBitrate, directPlayProfiles: directPlayProfiles, transcodingProfiles: transcodingProfiles, containerProfiles: [], codecProfiles: codecProfiles, responseProfiles: responseProfiles, subtitleProfiles: subtitleProfiles) - return DeviceProfileRoot(DeviceProfile: DP) + return profile } private func supportsFeature(minimumSupported: CPUModel) -> Bool { diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index babb493d..5512f53a 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -37,8 +37,8 @@ struct EpisodeItemView: View { formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" let request = RestRequest(method: .post, - url: (globalData.server?.baseURI ?? "") + - "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") + url: (globalData.server.baseURI ?? "") + + "/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -47,8 +47,8 @@ struct EpisodeItemView: View { } } else { let request = RestRequest(method: .delete, - url: (globalData.server?.baseURI ?? "") + - "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)") + url: (globalData.server.baseURI ?? "") + + "/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -64,8 +64,8 @@ struct EpisodeItemView: View { didSet { if favorite == true { let request = RestRequest(method: .post, - url: (globalData.server?.baseURI ?? "") + - "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") + url: (globalData.server.baseURI ?? "") + + "/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -74,8 +74,8 @@ struct EpisodeItemView: View { } } else { let request = RestRequest(method: .delete, - url: (globalData.server?.baseURI ?? "") + - "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") + url: (globalData.server.baseURI ?? "") + + "/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -96,9 +96,9 @@ struct EpisodeItemView: View { return } _viewDidLoad.wrappedValue = true - let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)" + let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)" - let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) + let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url) request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -152,7 +152,7 @@ struct EpisodeItemView: View { cast.Role = person["Role"].string ?? "" cast .Image = - URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")! + URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")! fullItem.Cast.append(cast) } } @@ -203,7 +203,7 @@ struct EpisodeItemView: View { } var portraitHeaderView: some View { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem @@ -219,7 +219,7 @@ struct EpisodeItemView: View { var portraitHeaderOverlayView: some View { VStack(alignment: .leading) { HStack(alignment: .bottom, spacing: 12) { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : @@ -420,7 +420,7 @@ struct EpisodeItemView: View { } else { GeometryReader { geometry in ZStack { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem @@ -441,7 +441,7 @@ struct EpisodeItemView: View { .blur(radius: 2) HStack { VStack { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index 877eead7..393c67dd 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -7,25 +7,29 @@ import SwiftUI import Introspect +import JellyfinAPI -class ItemPlayback: ObservableObject { - @Published var shouldPlay: Bool = false; - @Published var itemToPlay: DetailItem = DetailItem(); +//good lord the environmental modifiers ;P + +class VideoPlayerItem: ObservableObject { + @Published var shouldShowPlayer: Bool = false; + @Published var itemToPlay: BaseItemDto = BaseItemDto(); } struct ItemView: View { - var item: ResumeItem; - @StateObject private var playback: ItemPlayback = ItemPlayback() - @State private var shouldShowLoadingView: Bool = false; + private var item: BaseItemDto; + @StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem() - init(item: ResumeItem) { + @State private var isLoading: Bool = false; //This variable is only changed by the underlying VLC view. + + init(item: BaseItemDto) { self.item = item; } var body: some View { - if(playback.shouldPlay) { - LoadingViewNoBlur(isShowing: $shouldShowLoadingView) { - VLCPlayerWithControls(item: playback.itemToPlay, loadBinding: $shouldShowLoadingView, pBinding: _playback.projectedValue.shouldPlay) + if(videoPlayerItem.shouldShowPlayer) { + LoadingViewNoBlur(isShowing: $isLoading) { + VLCPlayerWithControls(item: playback.itemToPlay, loadBinding: $isLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer) }.navigationBarHidden(true) .navigationBarBackButtonHidden(true) .statusBar(hidden: true) @@ -34,7 +38,6 @@ struct ItemView: View { .edgesIgnoringSafeArea(.all) .overrideViewPreference(.unspecified) .supportedOrientations(.landscape) - } else { Group { if(item.Type == "Movie") { @@ -50,7 +53,7 @@ struct ItemView: View { } } .introspectTabBarController { (UITabBarController) in - UITabBarController.tabBar.isHidden = false + UITabBarController.tabBar.isHidden = false } .navigationBarHidden(false) .navigationBarBackButtonHidden(false) diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift index e038ec1e..baf0571d 100644 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/JellyfinPlayerApp.swift @@ -169,24 +169,6 @@ extension View { } } -extension String { - public func leftPad(toWidth width: Int, withString string: String?) -> String { - let paddingString = string ?? " " - - if self.count >= width { - return self - } - - let remainingLength: Int = width - self.count - var padString = String() - for _ in 0 ..< remainingLength { - padString += paddingString - } - - return "\(padString)\(self)" - } -} - @main struct JellyfinPlayerApp: App { let persistenceController = PersistenceController.shared diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index cc11808f..cf6b7c4b 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -6,148 +6,68 @@ */ import SwiftUI -import SwiftyRequest -import SwiftyJSON import NukeUI +import JellyfinAPI struct LatestMediaView: View { - @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject var globalData: GlobalData - @State var resumeItems: [ResumeItem] = [] + @State var items: [BaseItemDto] = [] private var library_id: String = ""; - @State private var viewDidLoad: Int = 0; + @State private var viewDidLoad: Bool = false; - init(library: String) { - library_id = library; - } - - init() { - library_id = ""; + init(usingLibraryID: String) { + library_id = usingLibraryID; } func onAppear() { - if(globalData.server?.baseURI == "") { + if(viewDidLoad == true) { return } - if(viewDidLoad == 1) { - return - } - _viewDidLoad.wrappedValue = 1; - let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/Items/Latest?Limit=12&IncludeItemTypes=Movie%2CSeries&Limit=16&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo%2CPath&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&ParentId=\(library_id)") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" + viewDidLoad = true; - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let response): - let body = response.body - do { - let json = try JSON(data: body) - for (_,item):(String, JSON) in json { - // Do something you want - var itemObj = ResumeItem() - itemObj.Image = item["ImageTags"]["Primary"].string ?? "" - itemObj.ImageType = "Primary" - itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" - itemObj.Name = item["Name"].string ?? "" - itemObj.Type = item["Type"].string ?? "" - itemObj.IndexNumber = item["IndexNumber"].int ?? nil - itemObj.Id = item["Id"].string ?? "" - itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil - itemObj.SeasonId = item["SeasonId"].string ?? nil - itemObj.SeriesId = item["SeriesId"].string ?? nil - itemObj.SeriesName = item["SeriesName"].string ?? nil - itemObj.Watched = item["UserData"]["Played"].bool ?? false - - if(itemObj.Type == "Series") { - itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0 - } - - if(itemObj.Type != "Episode") { - _resumeItems.wrappedValue.append(itemObj) - } - } - //print("latestmediaview done https") - } catch { - - } - break - case .failure(let error): - debugPrint(error) - _viewDidLoad.wrappedValue = 0; - break - } - } + UserLibraryAPI.getLatestMedia(userId: globalData.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio,.seriesPrimaryImage], enableUserData: true, limit: 12) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { response in + items = response + }) + .store(in: &globalData.pendingAPIRequests) } var body: some View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack() { - Spacer().frame(width:14) - ForEach(resumeItems, id: \.Id) { item in - NavigationLink(destination: ItemView(item: item)) { - VStack(alignment: .leading) { - if(item.Type == "Series") { + Spacer().frame(width:16) + ForEach(items, id: \.id) { item in + if(item.type == "Series" || item.type == "Movie") { + NavigationLink(destination: EmptyView()) { + VStack(alignment: .leading) { Spacer().frame(height:10) - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) + LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) + Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!) .resizable() .frame(width: 100, height: 150) .cornerRadius(10) } .frame(width: 100, height: 150) .cornerRadius(10) - .overlay( - ZStack { - if(item.ItemBadge == 0) { - Image(systemName: "checkmark") - .font(.caption) - .padding(3) - .foregroundColor(.white) - } else { - Text("\(String(item.ItemBadge ?? 0))") - .font(.caption) - .padding(3) - .foregroundColor(.white) - } - }.background(Color.black) - .opacity(0.8) - .cornerRadius(10.0) - .padding(3), alignment: .topTrailing - - ) - } else { - Spacer().frame(height:10) - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) - .resizable() - .frame(width: 100, height: 150) - .cornerRadius(10) - } - .frame(width: 100, height: 150) - .cornerRadius(10) - } - Text(item.Name) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - Spacer().frame(height:5) - }.frame(width: 100) + Spacer().frame(height:5) + Text(item.seasonName ?? item.name ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + }.frame(width: 100) + Spacer().frame(width: 15) + } } - Spacer().frame(width: 14) } - }.frame(height: 190) - }.onAppear(perform: onAppear).padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 190) - } -} - -struct LatestMediaView_Previews: PreviewProvider { - static var previews: some View { - LatestMediaView() + } + .frame(height: 190) + } + .onAppear(perform: onAppear) + .padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 190) } } diff --git a/JellyfinPlayer/LibraryFilterView.swift b/JellyfinPlayer/LibraryFilterView.swift index 27ec9e03..e98d28ee 100644 --- a/JellyfinPlayer/LibraryFilterView.swift +++ b/JellyfinPlayer/LibraryFilterView.swift @@ -65,8 +65,8 @@ struct LibraryFilterView: View { _sortOrder.wrappedValue = filter.asc?.rawValue ?? sortOrder _allGenres.wrappedValue = [] - let url = "/Items/Filters?UserId=\(globalData.user?.user_id ?? "")&ParentId=\(library)" - let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) + let url = "/Items/Filters?UserId=\(globalData.user.user_id ?? "")&ParentId=\(library)" + let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url) request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index 8fa4ecf2..6f44d548 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -87,7 +87,7 @@ struct ResumeItemGridCell: View { var body: some View { VStack(alignment: .leading) { if item.Type == "Movie" { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item @@ -100,7 +100,7 @@ struct ResumeItemGridCell: View { .frame(width: 100, height: 150) .cornerRadius(10) } else { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index d4a103ce..1d72c971 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -136,7 +136,7 @@ extension LibraryView { var body: some View { VStack(alignment: .leading) { if item.Type == "Movie" { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item @@ -149,7 +149,7 @@ extension LibraryView { .frame(width: 100, height: 150) .cornerRadius(10) } else { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index b41b01f2..3666d322 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -125,11 +125,11 @@ struct MovieItemView: View { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone(secondsFromGMT: 0) - print((globalData.server?.baseURI ?? "") + - "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") + print((globalData.server.baseURI ?? "") + + "/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") let request = RestRequest(method: .post, - url: (globalData.server?.baseURI ?? "") + - "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") + url: (globalData.server.baseURI ?? "") + + "/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -138,8 +138,8 @@ struct MovieItemView: View { } } else { let request = RestRequest(method: .delete, - url: (globalData.server?.baseURI ?? "") + - "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)") + url: (globalData.server.baseURI ?? "") + + "/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -155,8 +155,8 @@ struct MovieItemView: View { didSet { if favorite == true { let request = RestRequest(method: .post, - url: (globalData.server?.baseURI ?? "") + - "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") + url: (globalData.server.baseURI ?? "") + + "/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -165,8 +165,8 @@ struct MovieItemView: View { } } else { let request = RestRequest(method: .delete, - url: (globalData.server?.baseURI ?? "") + - "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") + url: (globalData.server.baseURI ?? "") + + "/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -187,9 +187,9 @@ struct MovieItemView: View { return } _viewDidLoad.wrappedValue = true - let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)" + let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)" - let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) + let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url) request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -242,7 +242,7 @@ struct MovieItemView: View { cast.Role = person["Role"].string ?? "" cast .Image = - URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=250&quality=85&tag=\(imageTag)")! + URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=250&quality=85&tag=\(imageTag)")! fullItem.Cast.append(cast) } } @@ -293,7 +293,7 @@ struct MovieItemView: View { } var portraitHeaderView: some View { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem @@ -309,7 +309,7 @@ struct MovieItemView: View { var portraitHeaderOverlayView: some View { VStack(alignment: .leading) { HStack(alignment: .bottom, spacing: 12) { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : @@ -510,7 +510,7 @@ struct MovieItemView: View { } else { GeometryReader { geometry in ZStack { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem @@ -531,7 +531,7 @@ struct MovieItemView: View { .blur(radius: 2) HStack { VStack { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index d85f3d8d..9afd044c 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -6,109 +6,73 @@ */ import SwiftUI -import SwiftyRequest -import SwiftyJSON import NukeUI +import JellyfinAPI struct NextUpView: View { - @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject var globalData: GlobalData - @State var resumeItems: [ResumeItem] = [] - @State private var viewDidLoad: Int = 0; - @State private var isLoading: Bool = false; + @State private var items: [BaseItemDto] = [] + @State private var viewDidLoad: Bool = false; func onAppear() { - if(globalData.server?.baseURI == "") { + if(viewDidLoad == true) { return } - if(viewDidLoad == 1) { - return - } - _viewDidLoad.wrappedValue = 1; - let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Shows/NextUp?Limit=12&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&MediaTypes=Video&UserId=\(globalData.user?.user_id ?? "")") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" + viewDidLoad = true; - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let response): - let body = response.body - do { - let json = try JSON(data: body) - for (_,item):(String, JSON) in json["Items"] { - // Do something you want - var itemObj = ResumeItem() - itemObj.Image = item["SeriesPrimaryImageTag"].string ?? "" - itemObj.ImageType = "Primary" - itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" - itemObj.Name = item["Name"].string ?? "" - itemObj.Type = item["Type"].string ?? "" - itemObj.IndexNumber = item["IndexNumber"].int ?? nil - itemObj.Id = item["Id"].string ?? "" - itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil - itemObj.SeasonId = item["SeasonId"].string ?? nil - itemObj.SeriesId = item["SeriesId"].string ?? nil - itemObj.SeriesName = item["SeriesName"].string ?? nil - - _resumeItems.wrappedValue.append(itemObj) - } - _isLoading.wrappedValue = false; - } catch { - - } - break - case .failure(let error): - debugPrint(error) - _viewDidLoad.wrappedValue = 0; - break - } - } + TvShowsAPI.getNextUp(userId: globalData.user.user_id!, limit: 12) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { response in + items = response.items ?? [] + }) + .store(in: &globalData.pendingAPIRequests) } var body: some View { VStack(alignment: .leading) { - if(resumeItems.count != 0) { - Text("Next Up").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + if(items.count != 0) { + Text("Next Up") + .font(.title2) + .fontWeight(.bold) + .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) ScrollView(.horizontal, showsIndicators: false) { LazyHStack() { - if(isLoading == false) { - Spacer().frame(width:14) - ForEach(resumeItems, id: \.Id) { item in - NavigationLink(destination: ItemView(item: item)) { - VStack(alignment: .leading) { - Spacer().frame(height:10) - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.SeriesId ?? "")/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) - .resizable() - .frame(width: 100, height: 150) - .cornerRadius(10) - } - .frame(width: 100, height: 150) - .cornerRadius(10) - Text(item.SeriesName ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0))") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - Spacer().frame(height:5) - } - .frame(width: 100) - Spacer().frame(width:12) - } - Spacer().frame(width: 10) + Spacer().frame(width:16) + ForEach(items, id: \.id) { item in + NavigationLink(destination: EmptyView()) { + VStack(alignment: .leading) { + LazyImage(source: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100)) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: item.getSeriesPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!) + .resizable() + .frame(width: 100, height: 150) + .cornerRadius(10) + } + .frame(width: 100, height: 150) + .cornerRadius(10) + Spacer().frame(height:5) + Text(item.seriesName!) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + Text("S\(item.parentIndexNumber ?? 0):E\(item.indexNumber ?? 0)") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + }.frame(width: 100) + Spacer().frame(width:16) } } - }.frame(height: 200) - }.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 200) + } + } + .frame(height: 200) } - }.onAppear(perform: onAppear).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + } + .onAppear(perform: onAppear) + .padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) } } diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 413e923a..65c810b9 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -35,9 +35,9 @@ struct SeasonItemView: View { if hasAppearedOnce { return } - let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)" + let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)" - let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) + let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url) request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -84,7 +84,7 @@ struct SeasonItemView: View { cast.Role = person["Role"].string ?? "" cast .Image = - URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=2000&quality=90&tag=\(imageTag)")! + URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=2000&quality=90&tag=\(imageTag)")! responseItem.Cast.append(cast) } } @@ -92,8 +92,8 @@ struct SeasonItemView: View { _fullItem.wrappedValue = responseItem let url2 = - "/Shows/\(fullItem.SeriesId ?? "")/Episodes?SeasonId=\(item.Id)&UserId=\(globalData.user?.user_id ?? "")&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CBasicSyncInfo%2CCanDelete%2CMediaSourceCount%2COverview" - let request2 = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url2) + "/Shows/\(fullItem.SeriesId ?? "")/Episodes?SeasonId=\(item.Id)&UserId=\(globalData.user.user_id ?? "")&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CBasicSyncInfo%2CCanDelete%2CMediaSourceCount%2COverview" + let request2 = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url2) request2.headerParameters["X-Emby-Authorization"] = globalData.authHeader request2.contentType = "application/json" request2.acceptType = "application/json" @@ -193,7 +193,7 @@ struct SeasonItemView: View { if isLoading { EmptyView() } else { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=550&quality=90&tag=\(item.SeasonImage ?? "")")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=550&quality=90&tag=\(item.SeasonImage ?? "")")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item .SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item @@ -209,7 +209,7 @@ struct SeasonItemView: View { var portraitHeaderOverlayView: some View { HStack(alignment: .bottom, spacing: 12) { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem @@ -258,7 +258,7 @@ struct SeasonItemView: View { ForEach(episodes, id: \.Id) { episode in NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) { HStack { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: episode .PosterBlurHash == "" ? @@ -276,7 +276,7 @@ struct SeasonItemView: View { .overlay( Rectangle() .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .mask(CustomShape(radius: 10)) + .mask(ProgressBar()) .frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * 150), height: 7) .padding(0), alignment: .bottomLeading ) @@ -333,7 +333,7 @@ struct SeasonItemView: View { } else { GeometryReader { geometry in ZStack { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(item.SeasonImage ?? "")")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(item.SeasonImage ?? "")")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item .SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item @@ -355,7 +355,7 @@ struct SeasonItemView: View { HStack { VStack(alignment: .leading) { Spacer().frame(height: 16) - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : @@ -392,7 +392,7 @@ struct SeasonItemView: View { ForEach(episodes, id: \.Id) { episode in NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) { HStack { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: episode .PosterBlurHash == "" ? @@ -410,7 +410,7 @@ struct SeasonItemView: View { .overlay( Rectangle() .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .mask(CustomShape(radius: 10)) + .mask(ProgressBar()) .frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * 150), height: 7) .padding(0), alignment: .bottomLeading ) diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 3d9b9e35..6bce33be 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -22,9 +22,9 @@ struct SeriesItemView: View { return; } _isLoading.wrappedValue = true; - let url = "/Shows/\(item.Id )/Seasons?userId=\(globalData.user?.user_id ?? "")&Fields=ItemCount" + let url = "/Shows/\(item.Id )/Seasons?userId=\(globalData.user.user_id ?? "")&Fields=ItemCount" - let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) + let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url) request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" @@ -97,7 +97,7 @@ struct SeriesItemView: View { ForEach(items, id: \.Id) { item in NavigationLink(destination: ItemView(item: item )) { VStack(alignment: .leading) { - LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=90&tag=\(item.Image)")) + LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=90&tag=\(item.Image)")) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!) .resizable() diff --git a/JellyfinPlayer/Views/SettingsView.swift b/JellyfinPlayer/Views/SettingsView.swift index ca1afece..a6cd298d 100644 --- a/JellyfinPlayer/Views/SettingsView.swift +++ b/JellyfinPlayer/Views/SettingsView.swift @@ -32,7 +32,7 @@ struct SettingsView: View { private var autoSelectSubtitlesLangcode: String = "none" func onAppear() { - _username.wrappedValue = globalData.user?.username ?? "" + _username.wrappedValue = globalData.user.username ?? "" let defaults = UserDefaults.standard _inNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "InNetworkBandwidth") _outOfNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "OutOfNetworkBandwidth") @@ -94,8 +94,8 @@ struct SettingsView: View { // TODO: handle the error } - globalData.server = nil - globalData.user = nil + globalData.server = Server() + globalData.user = SignedInUser() globalData.authToken = "" globalData.authHeader = "" jsi.did = true diff --git a/JellyfinPlayer/Views/VideoPlayer.swift b/JellyfinPlayer/Views/VideoPlayer.swift index e8fbf8d5..e6552e61 100644 --- a/JellyfinPlayer/Views/VideoPlayer.swift +++ b/JellyfinPlayer/Views/VideoPlayer.swift @@ -7,19 +7,13 @@ import SwiftUI import MobileVLCKit -import SwiftyJSON -import SwiftyRequest - -enum VideoType { - case hls; - case direct; -} +import JellyfinAPI struct Subtitle { var name: String; var id: Int32; var url: URL; - var delivery: String; + var delivery: SubtitleDeliveryMethod; var codec: String; } @@ -29,7 +23,7 @@ struct AudioTrack { } class PlaybackItem: ObservableObject { - @Published var videoType: VideoType = .hls; + @Published var videoType: PlayMethod = .directPlay; @Published var videoUrl: URL = URL(string: "https://example.com")!; } @@ -81,7 +75,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe var subtitleTrackArray: [Subtitle] = []; var audioTrackArray: [AudioTrack] = []; - var manifest: DetailItem = DetailItem(); + var manifest: BaseItemDto = BaseItemDto(); var playbackItem = PlaybackItem(); @IBAction func seekSliderStart(_ sender: Any) { @@ -199,8 +193,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe //Rotate to landscape only if necessary UIViewController.attemptRotationToDeviceOrientation(); - //Show loading screen - mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14) //mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate") @@ -208,7 +200,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe mediaPlayer.delegate = self mediaPlayer.drawable = videoContentView - titleLabel.text = manifest.Name + titleLabel.text = manifest.name //Fetch max bitrate from UserDefaults depending on current connection mode let defaults = UserDefaults.standard @@ -219,118 +211,88 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe builder.setMaxBitrate(bitrate: maxBitrate) let profile = builder.buildProfile() - let jsonEncoder = JSONEncoder() - let jsonData = try! jsonEncoder.encode(profile) + let playbackInfo = PlaybackInfoDto(userId: globalData.user.user_id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) - let url = (globalData.server?.baseURI ?? "") + "/Items/\(manifest.Id)/PlaybackInfo?UserId=\(globalData.user?.user_id ?? "")&StartTimeTicks=\(Int(manifest.Progress))&IsPlayback=true&AutoOpenLiveStream=true&MaxStreamingBitrate=\(profile.DeviceProfile.MaxStreamingBitrate)"; - - let request = RestRequest(method: .post, url: url) - - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = jsonData - - request.responseData() { [self] (result: Result, RestError>) in - switch result { - case .success(let response): - let body = response.body - do { - let json = try JSON(data: body) - playSessionId = json["PlaySessionId"].string ?? ""; - if(json["MediaSources"][0]["TranscodingUrl"].string != nil) { - let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")\((json["MediaSources"][0]["TranscodingUrl"].string ?? ""))")! - let item = PlaybackItem() - item.videoType = .hls - item.videoUrl = streamURL - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "") - subtitleTrackArray.append(disableSubtitleTrack); - - for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] { - if(stream["Type"].string == "Subtitle") { //ignore ripped subtitles - we don't want to extract subtitles - let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")! - let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "") - subtitleTrackArray.append(subtitle); - } - - if(stream["Type"].string == "Audio") { - let subtitle = AudioTrack(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0)) - if(stream["IsDefault"].boolValue) { - selectedAudioTrack = Int32(stream["Index"].int ?? 0); - } - audioTrackArray.append(subtitle); - } + MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: globalData.user.user_id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: self.globalData, completion: completion) + }, receiveValue: { [self] response in + playSessionId = response.playSessionId! + let mediaSource = response.mediaSources!.first.self! + if(mediaSource.transcodingUrl != nil) { + //Item is being transcoded by request of server + let streamURL = URL(string: "\(globalData.server.baseURI!)\(mediaSource.transcodingUrl!)") + let item = PlaybackItem() + item.videoType = .transcode + item.videoUrl = streamURL! + + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "") + subtitleTrackArray.append(disableSubtitleTrack); + + //Loop through media streams and add to array + for stream in mediaSource.mediaStreams! { + if(stream.type == .subtitle) { + let deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")! + let subtitle = Subtitle(name: stream.displayTitle!, id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!) + subtitleTrackArray.append(subtitle); } - if(selectedAudioTrack == -1) { - if(audioTrackArray.count > 0) { - selectedAudioTrack = audioTrackArray[0].id; + if(stream.type == .audio) { + let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!)) + if(stream.isDefault! == true) { + selectedAudioTrack = Int32(stream.index!); } + audioTrackArray.append(subtitle); } - - self.sendPlayReport() - playbackItem = item; - } else { - print("Direct playing!"); - let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")/Videos/\(manifest.Id)/stream?Static=true&mediaSourceId=\(manifest.Id)&deviceId=\(globalData.user?.device_uuid ?? "")&api_key=\(globalData.authToken)&Tag=\(json["MediaSources"][0]["ETag"])")!; - let item = PlaybackItem() - item.videoUrl = streamURL - item.videoType = .direct - - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "") - subtitleTrackArray.append(disableSubtitleTrack); - for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] { - if(stream["Type"].string == "Subtitle") { - let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")! - let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "") - subtitleTrackArray.append(subtitle); - } - - if(stream["Type"].string == "Audio") { - let subtitle = AudioTrack(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0)) - if(stream["IsDefault"].boolValue) { - selectedAudioTrack = Int32(stream["Index"].int ?? 0); - } - audioTrackArray.append(subtitle); - } - } - - if(selectedAudioTrack == -1) { - if(audioTrackArray.count > 0) { - selectedAudioTrack = audioTrackArray[0].id; - } - } - - sendPlayReport() - playbackItem = item; } - DispatchQueue.global(qos: .background).async { - mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) - mediaPlayer.play() - mediaPlayer.jumpForward(Int32(manifest.Progress/10000000)) - mediaPlayer.pause() - subtitleTrackArray.forEach() { sub in - if(sub.id != -1 && sub.delivery == "External" && sub.codec != "subrip") { - print("adding subs for id: \(sub.id) w/ url: \(sub.url)") - mediaPlayer.addPlaybackSlave(sub.url, type: .subtitle, enforce: false) - } + if(selectedAudioTrack == -1) { + if(audioTrackArray.count > 0) { + selectedAudioTrack = audioTrackArray[0].id; } - delegate?.showLoadingView(self) - while(mediaPlayer.numberOfSubtitlesTracks != subtitleTrackArray.count - 1) {} - mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack; - mediaPlayer.pause() - mediaPlayer.play() } - } catch { + self.sendPlayReport() + playbackItem = item; + } else { + //Item will be directly played by the client. + let streamURL: URL = URL(string: "\(globalData.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(globalData.user.device_uuid!)&api_key=\(globalData.authToken)&Tag=\(mediaSource.eTag!)")!; + + let item = PlaybackItem() + item.videoUrl = streamURL + item.videoType = .directPlay + + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "") + subtitleTrackArray.append(disableSubtitleTrack); + + //Loop through media streams and add to array + for stream in mediaSource.mediaStreams! { + if(stream.type == .subtitle) { + let deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")! + let subtitle = Subtitle(name: stream.displayTitle!, id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!) + subtitleTrackArray.append(subtitle); + } + + if(stream.type == .audio) { + let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!)) + if(stream.isDefault! == true) { + selectedAudioTrack = Int32(stream.index!); + } + audioTrackArray.append(subtitle); + } + } + + if(selectedAudioTrack == -1) { + if(audioTrackArray.count > 0) { + selectedAudioTrack = audioTrackArray[0].id; + } + } + + self.sendPlayReport() + playbackItem = item; } - break - case .failure(let error): - debugPrint(error) - break - } - } + }) + .store(in: &globalData.pendingAPIRequests) } override func viewWillAppear(_ animated: Bool) { @@ -430,74 +392,46 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe //MARK: Jellyfin Playstate updates func sendProgressReport(eventName: String) { - var progressBody: String = ""; - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":\(mediaPlayer.state == .paused ? "true" : "false"),\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(mediaPlayer.position * Float(manifest.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":569735888.888889}],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"EventName\":\"\(eventName)\"}"; + let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Progress") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = progressBody.data(using: .ascii); - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let resp): - print(resp.body) - break - case .failure(let error): - debugPrint(error) - break - } - } + PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: self.globalData, completion: completion) + }, receiveValue: { response in + print("Playback progress report sent!") + }) + .store(in: &globalData.pendingAPIRequests) } func sendStopReport() { - var progressBody: String = ""; + let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: []) - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":true,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(mediaPlayer.position * Float(manifest.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":100000}],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(manifest.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; - - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Stopped") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = progressBody.data(using: .ascii); - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let resp): - print(resp.body) - break - case .failure(let error): - debugPrint(error) - break - } - } + PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: self.globalData, completion: completion) + }, receiveValue: { response in + print("Playback stop report sent!") + }) + .store(in: &globalData.pendingAPIRequests) } func sendPlayReport() { - var progressBody: String = ""; startTime = Int(Date().timeIntervalSince1970) * 10000000 - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":false,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(manifest.Progress)),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(manifest.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; + let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = progressBody.data(using: .ascii); - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let resp): - print(resp.body) - break - case .failure(let error): - debugPrint(error) - break - } - } + PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: self.globalData, completion: completion) + }, receiveValue: { response in + print("Playback start report sent!") + }) + .store(in: &globalData.pendingAPIRequests) } } struct VLCPlayerWithControls: UIViewControllerRepresentable { - var item: DetailItem + var item: BaseItemDto @Environment(\.presentationMode) var presentationMode @EnvironmentObject private var globalData: GlobalData; diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift new file mode 100644 index 00000000..94fea2e1 --- /dev/null +++ b/Shared/Extensions/APIExtensions.swift @@ -0,0 +1,81 @@ +/* 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 +import UIKit + +//001fC^ = dark grey plain blurhash + +extension BaseItemDto { + func getSeriesPrimaryImageBlurHash() -> String { + let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString; + let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]; + + return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"; + } + + func getPrimaryImageBlurHash() -> String { + let rawImgURL = self.getPrimaryImage(baseURL: "", maxWidth: 1).absoluteString; + let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]; + + return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"; + } + + func getBackdropImageBlurHash() -> String { + let rawImgURL = self.getBackdropImage(baseURL: "", maxWidth: 1).absoluteString; + let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]; + + if(rawImgURL.contains("Backdrop")) { + return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"; + } else { + return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"; + } + } + + func getBackdropImage(baseURL: String, maxWidth: Int) -> URL { + var imageType = ""; + var imageTag = ""; + + if(self.primaryImageAspectRatio ?? 0.0 < 1.0) { + imageType = "Backdrop"; + imageTag = (self.backdropImageTags ?? [""])[0] + } else { + imageType = "Primary"; + imageTag = self.imageTags?["Primary"] ?? "" + } + + if(imageTag == "") { + imageType = "Backdrop"; + imageTag = self.parentBackdropImageTags?[0] ?? "" + } + let x = UIScreen.main.nativeScale * CGFloat(maxWidth) + let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)" + return URL(string: urlString)! + } + + func getSeriesPrimaryImage(baseURL: String, maxWidth: Int) -> URL { + let imageType = "Primary"; + let imageTag = self.seriesPrimaryImageTag ?? "" + let x = UIScreen.main.nativeScale * CGFloat(maxWidth) + let urlString = "\(baseURL)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)" + return URL(string: urlString)! + } + + func getPrimaryImage(baseURL: String, maxWidth: Int) -> URL { + let imageType = "Primary"; + var imageTag = self.imageTags?["Primary"] ?? ""; + + if(imageTag == "") { + imageTag = self.seriesPrimaryImageTag ?? "" + } + let x = UIScreen.main.nativeScale * CGFloat(maxWidth) + + let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)" + return URL(string: urlString)! + } +} diff --git a/Shared/Extensions/MultiSelector.swift b/Shared/Extensions/MultiSelectorView.swift similarity index 100% rename from Shared/Extensions/MultiSelector.swift rename to Shared/Extensions/MultiSelectorView.swift diff --git a/Shared/Extensions/LazyImage.swift b/Shared/Extensions/NukeExtensions.swift similarity index 100% rename from Shared/Extensions/LazyImage.swift rename to Shared/Extensions/NukeExtensions.swift diff --git a/Shared/Extensions/String.swift b/Shared/Extensions/StringExtensions.swift similarity index 62% rename from Shared/Extensions/String.swift rename to Shared/Extensions/StringExtensions.swift index 3e0797c0..9ade1f72 100644 --- a/Shared/Extensions/String.swift +++ b/Shared/Extensions/StringExtensions.swift @@ -15,4 +15,21 @@ extension String { return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith) } catch { return self } } + + func leftPad(toWidth width: Int, withString string: String?) -> String { + let paddingString = string ?? " " + + if self.count >= width { + return self + } + + let remainingLength: Int = width - self.count + var padString = String() + for _ in 0 ..< remainingLength { + padString += paddingString + } + + return "\(padString)\(self)" + } } + diff --git a/Shared/Resources/Model.xcdatamodeld/JellyfinPlayer.xcdatamodel/contents b/Shared/Resources/Model.xcdatamodeld/JellyfinPlayer.xcdatamodel/contents index f81c5937..6b3aa76a 100644 --- a/Shared/Resources/Model.xcdatamodeld/JellyfinPlayer.xcdatamodel/contents +++ b/Shared/Resources/Model.xcdatamodeld/JellyfinPlayer.xcdatamodel/contents @@ -1,14 +1,14 @@ - + - - - + + + - - - + + + diff --git a/Shared/Typings/Typings.swift b/Shared/Typings/Typings.swift index bfdde290..77338969 100644 --- a/Shared/Typings/Typings.swift +++ b/Shared/Typings/Typings.swift @@ -13,9 +13,9 @@ class justSignedIn: ObservableObject { } class GlobalData: ObservableObject { - @Published var user: SignedInUser? + @Published var user: SignedInUser = SignedInUser() @Published var authToken: String = "" - @Published var server: Server? + @Published var server: Server = Server() @Published var authHeader: String = "" @Published var isInNetwork: Bool = true; @Published var networkError: Bool = false;