From 60c702742958b999ec80c819e84b02e9559e9222 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 24 Oct 2021 11:16:04 +0200 Subject: [PATCH] Open videos via URL scheme --- Model/Player/PlayerQueue.swift | 17 +- Model/Player/PlayerSegments.swift | 6 +- Model/SponsorBlock/SponsorBlockAPI.swift | 4 +- Pearvidious.xcodeproj/project.pbxproj | 153 +++--------------- .../xcschemes/Pearvidious (macOS).xcscheme | 10 -- Shared/AppDelegate.swift | 8 - Shared/Defaults.swift | 12 +- Shared/Navigation/ContentView.swift | 23 +++ Shared/PearvidiousApp.swift | 5 + Shared/VideoURLParser.swift | 78 +++++++++ Shared/Videos/HorizontalCells.swift | 1 - Shared/Videos/VerticalCells.swift | 1 - .../Extensions/Int+FormatTests.swift | 0 Tests macOS/Tests_macOS.swift | 22 --- Tests macOS/VideoURLParserTests.swift | 39 +++++ iOS/Info.plist | 13 ++ macOS/AppDelegate.swift | 8 + macOS/Info.plist | 19 +++ 18 files changed, 230 insertions(+), 189 deletions(-) delete mode 100644 Shared/AppDelegate.swift create mode 100644 Shared/VideoURLParser.swift rename {Shared Tests => Tests macOS}/Extensions/Int+FormatTests.swift (100%) create mode 100644 Tests macOS/VideoURLParserTests.swift create mode 100644 macOS/AppDelegate.swift create mode 100644 macOS/Info.plist diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index 0a549e9a..a9da5f5f 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -27,17 +27,20 @@ extension PlayerModel { } } - func playNow(_ video: Video) { + func playNow(_ video: Video, at time: TimeInterval? = nil) { addCurrentItemToHistory() enqueueVideo(video, prepending: true) { _, item in - self.advanceToItem(item) + self.advanceToItem(item, at: time) } } - func playItem(_ item: PlayerQueueItem, video: Video? = nil) { + func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) { currentItem = item - if currentItem.playbackTime.isNil { + + if !time.isNil { + currentItem.playbackTime = CMTime(seconds: time!, preferredTimescale: 1_000_000) + } else if currentItem.playbackTime.isNil { currentItem.playbackTime = .zero } @@ -45,7 +48,7 @@ extension PlayerModel { currentItem.video = video! } - playVideo(currentVideo!, time: item.playbackTime) + playVideo(currentVideo!, time: currentItem.playbackTime) } func advanceToNextItem() { @@ -56,12 +59,12 @@ extension PlayerModel { } } - func advanceToItem(_ newItem: PlayerQueueItem) { + func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) { addCurrentItemToHistory() let item = remove(newItem)! loadDetails(newItem.video) { video in - self.playItem(item, video: video) + self.playItem(item, video: video, at: time) } } diff --git a/Model/Player/PlayerSegments.swift b/Model/Player/PlayerSegments.swift index c48a301f..365731d4 100644 --- a/Model/Player/PlayerSegments.swift +++ b/Model/Player/PlayerSegments.swift @@ -32,14 +32,16 @@ extension PlayerModel { private func skip(_ segment: Segment, at time: CMTime) { guard segment.endTime.seconds <= playerItemDuration?.seconds ?? .infinity else { - logger.error("item time is: \(time.seconds) and trying to skip to \(playerItemDuration?.seconds ?? .infinity)") + logger.error( + "segment end time is: \(segment.end) when player item duration is: \(playerItemDuration?.seconds ?? .infinity)" + ) return } player.seek(to: segment.endTime) lastSkipped = segment segmentRestorationTime = time - logger.info("SponsorBlock skipping to: \(segment.endTime)") + logger.info("SponsorBlock skipping to: \(segment.end)") } private func shouldSkip(_ segment: Segment, at time: CMTime) -> Bool { diff --git a/Model/SponsorBlock/SponsorBlockAPI.swift b/Model/SponsorBlock/SponsorBlockAPI.swift index e0b7ef7e..7269b5e3 100644 --- a/Model/SponsorBlock/SponsorBlockAPI.swift +++ b/Model/SponsorBlock/SponsorBlockAPI.swift @@ -5,10 +5,10 @@ import Logging import SwiftyJSON final class SponsorBlockAPI: ObservableObject { - let logger = Logger(label: "net.yattee.app.sb") - static let categories = ["sponsor", "selfpromo", "intro", "outro", "interaction", "music_offtopic"] + let logger = Logger(label: "net.yattee.app.sb") + @Published var videoID: String? @Published var segments = [Segment]() diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index fd4bd401..f45bd28b 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -121,6 +121,7 @@ 374C053F272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; }; 374C0540272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; }; 374C0541272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; }; + 374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C0542272496E4009BDDBE /* AppDelegate.swift */; }; 375168D62700FAFF008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; 375168D82700FDB9008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; @@ -247,8 +248,6 @@ 37BA794F26DC3E0E002A0235 /* Int+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794E26DC3E0E002A0235 /* Int+Format.swift */; }; 37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794E26DC3E0E002A0235 /* Int+Format.swift */; }; 37BA795126DC3E0E002A0235 /* Int+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794E26DC3E0E002A0235 /* Int+Format.swift */; }; - 37BA796F26DC412E002A0235 /* Int+FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA796D26DC412E002A0235 /* Int+FormatTests.swift */; }; - 37BA797026DC426B002A0235 /* Int+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794E26DC3E0E002A0235 /* Int+Format.swift */; }; 37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */; }; 37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA42699FB72009BE4FB /* Alamofire */; }; 37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA6269A552E009BE4FB /* Alamofire */; }; @@ -296,6 +295,10 @@ 37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; }; + 37CB12792724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; }; + 37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; }; + 37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */; }; + 37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; }; 37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */; }; 37CC3F46270CE30600608308 /* PlayerQueueItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */; }; 37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */; }; @@ -384,13 +387,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 37BA796726DC40CB002A0235 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 37D4B0BD2671614700C925CA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 37D4B0CE2671614900C925CA; - remoteInfo = "Pearvidious (macOS)"; - }; 37D4B0D52671614900C925CA /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 37D4B0BD2671614700C925CA /* Project object */; @@ -447,6 +443,8 @@ 374C053427242D9F009BDDBE /* ServicesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesSettings.swift; sourceTree = ""; }; 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTVMenu.swift; sourceTree = ""; }; 374C053E272472C0009BDDBE /* PlayerSegments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSegments.swift; sourceTree = ""; }; + 374C0542272496E4009BDDBE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = macOS/AppDelegate.swift; sourceTree = SOURCE_ROOT; }; + 374C0544272496FD009BDDBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; 375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = ""; }; 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; }; @@ -490,7 +488,6 @@ 37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarSubscriptions.swift; sourceTree = ""; }; 37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarPlaylists.swift; sourceTree = ""; }; 37BA794E26DC3E0E002A0235 /* Int+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Format.swift"; sourceTree = ""; }; - 37BA796326DC40CB002A0235 /* Shared Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Shared Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 37BA796D26DC412E002A0235 /* Int+FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+FormatTests.swift"; sourceTree = ""; }; 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVNavigationView.swift; sourceTree = ""; }; 37BD07B42698AA4D003EBB87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -508,6 +505,8 @@ 37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChannelPlaylist+Fixtures.swift"; sourceTree = ""; }; 37C3A250272366440087A57A /* ChannelPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylistView.swift; sourceTree = ""; }; 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = ""; }; + 37CB12782724C76D00213B45 /* VideoURLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoURLParser.swift; sourceTree = ""; }; + 37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoURLParserTests.swift; sourceTree = ""; }; 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItem.swift; sourceTree = ""; }; 37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueView.swift; sourceTree = ""; }; 37CC3F4F270D010D00608308 /* VideoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoBanner.swift; sourceTree = ""; }; @@ -548,13 +547,6 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 37BA796026DC40CB002A0235 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 37D4B0C62671614900C925CA /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -819,14 +811,6 @@ path = iOS; sourceTree = ""; }; - 37BA796426DC40CB002A0235 /* Shared Tests */ = { - isa = PBXGroup; - children = ( - 37BA796C26DC4105002A0235 /* Extensions */, - ); - path = "Shared Tests"; - sourceTree = ""; - }; 37BA796C26DC4105002A0235 /* Extensions */ = { isa = PBXGroup; children = ( @@ -839,8 +823,10 @@ isa = PBXGroup; children = ( 37FD43E1270472060073EE42 /* Settings */, + 374C0542272496E4009BDDBE /* AppDelegate.swift */, 37BE0BDB26A2367F0092E2DB /* Player.swift */, 37BE0BD926A214630092E2DB /* PlayerViewController.swift */, + 374C0544272496FD009BDDBE /* Info.plist */, ); path = macOS; sourceTree = ""; @@ -868,7 +854,6 @@ 37D4B1B72672CFE300C925CA /* Model */, 37C7A9022679058300E721B4 /* Extensions */, 3748186426A762300084E870 /* Fixtures */, - 37BA796426DC40CB002A0235 /* Shared Tests */, 377FC7D1267A080300A6BBAF /* Frameworks */, 37D4B0CA2671614900C925CA /* Products */, 37D4B174267164B000C925CA /* Tests Apple TV */, @@ -895,6 +880,7 @@ 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */, 37D4B0C22671614700C925CA /* PearvidiousApp.swift */, 3700155E271B12DD0049C794 /* SiestaConfiguration.swift */, + 37CB12782724C76D00213B45 /* VideoURLParser.swift */, 37D4B0C42671614800C925CA /* Assets.xcassets */, 37BD07C42698ADEE003EBB87 /* Pearvidious.entitlements */, ); @@ -910,7 +896,6 @@ 37D4B0DE2671614900C925CA /* Tests (macOS).xctest */, 37D4B158267164AE00C925CA /* Pearvidious (tvOS).app */, 37D4B171267164B000C925CA /* Tests (tvOS).xctest */, - 37BA796326DC40CB002A0235 /* Shared Tests.xctest */, ); name = Products; sourceTree = ""; @@ -926,7 +911,9 @@ 37D4B0E12671614900C925CA /* Tests macOS */ = { isa = PBXGroup; children = ( + 37BA796C26DC4105002A0235 /* Extensions */, 37D4B0E22671614900C925CA /* Tests_macOS.swift */, + 37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */, ); path = "Tests macOS"; sourceTree = ""; @@ -1007,24 +994,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 37BA796226DC40CB002A0235 /* Shared Tests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 37BA796926DC40CB002A0235 /* Build configuration list for PBXNativeTarget "Shared Tests" */; - buildPhases = ( - 37BA795F26DC40CB002A0235 /* Sources */, - 37BA796026DC40CB002A0235 /* Frameworks */, - 37BA796126DC40CB002A0235 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 37BA796826DC40CB002A0235 /* PBXTargetDependency */, - ); - name = "Shared Tests"; - productName = "Shared Tests"; - productReference = 37BA796326DC40CB002A0235 /* Shared Tests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 37D4B0C82671614900C925CA /* Pearvidious (iOS) */ = { isa = PBXNativeTarget; buildConfigurationList = 37D4B0EC2671614900C925CA /* Build configuration list for PBXNativeTarget "Pearvidious (iOS)" */; @@ -1177,10 +1146,6 @@ LastSwiftUpdateCheck = 1300; LastUpgradeCheck = 1310; TargetAttributes = { - 37BA796226DC40CB002A0235 = { - CreatedOnToolsVersion = 13.0; - TestTargetID = 37D4B0CE2671614900C925CA; - }; 37D4B0C82671614900C925CA = { CreatedOnToolsVersion = 13.0; }; @@ -1241,7 +1206,6 @@ 37D4B0D32671614900C925CA /* Tests (iOS) */, 37D4B0DD2671614900C925CA /* Tests (macOS) */, 37D4B170267164B000C925CA /* Tests (tvOS) */, - 37BA796226DC40CB002A0235 /* Shared Tests */, 37FD43E62704A2240073EE42 /* Periphery (macOS) */, 37FD43EB2704A7710073EE42 /* Periphery (tvOS) */, ); @@ -1249,13 +1213,6 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 37BA796126DC40CB002A0235 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 37D4B0C72671614900C925CA /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1393,15 +1350,6 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 37BA795F26DC40CB002A0235 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 37BA797026DC426B002A0235 /* Int+Format.swift in Sources */, - 37BA796F26DC412E002A0235 /* Int+FormatTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 37D4B0C52671614900C925CA /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1409,6 +1357,7 @@ 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, + 37CB12792724C76D00213B45 /* VideoURLParser.swift in Sources */, 37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 3743CA52270F284F00E4D32B /* View+Borders.swift in Sources */, 3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, @@ -1566,6 +1515,7 @@ 3765788A2685471400D4EA09 /* Playlist.swift in Sources */, 37E2EEAC270656EC00170416 /* PlayerControlsView.swift in Sources */, 37E70924271CD43000D34DDE /* WelcomeScreen.swift in Sources */, + 374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */, 373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */, 37AAF29126740715007FC770 /* Channel.swift in Sources */, 376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */, @@ -1626,6 +1576,7 @@ 37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, + 37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */, 37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1642,6 +1593,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */, + 37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */, 37D4B0E32671614900C925CA /* Tests_macOS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1762,11 +1715,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 37BA796826DC40CB002A0235 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 37D4B0CE2671614900C925CA /* Pearvidious (macOS) */; - targetProxy = 37BA796726DC40CB002A0235 /* PBXContainerItemProxy */; - }; 37D4B0D62671614900C925CA /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 37D4B0C82671614900C925CA /* Pearvidious (iOS) */; @@ -1785,58 +1733,6 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 37BA796A26DC40CB002A0235 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 78Z5H3M6RJ; - GENERATE_INFOPLIST_FILE = YES; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "net.arekf.Shared-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = macosx; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Pearvidious.app/Contents/MacOS/Pearvidious"; - }; - name = Debug; - }; - 37BA796B26DC40CB002A0235 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 78Z5H3M6RJ; - GENERATE_INFOPLIST_FILE = YES; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "net.arekf.Shared-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = macosx; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Pearvidious.app/Contents/MacOS/Pearvidious"; - }; - name = Release; - }; 37D4B0EA2671614900C925CA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2027,6 +1923,7 @@ ENABLE_PREVIEWS = YES; ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = macOS/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; LD_RUNPATH_SEARCH_PATHS = ( @@ -2059,6 +1956,7 @@ ENABLE_PREVIEWS = YES; ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = macOS/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; LD_RUNPATH_SEARCH_PATHS = ( @@ -2333,15 +2231,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 37BA796926DC40CB002A0235 /* Build configuration list for PBXNativeTarget "Shared Tests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 37BA796A26DC40CB002A0235 /* Debug */, - 37BA796B26DC40CB002A0235 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 37D4B0C02671614700C925CA /* Build configuration list for PBXProject "Pearvidious" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Pearvidious.xcodeproj/xcshareddata/xcschemes/Pearvidious (macOS).xcscheme b/Pearvidious.xcodeproj/xcshareddata/xcschemes/Pearvidious (macOS).xcscheme index 86a08421..d240b41f 100644 --- a/Pearvidious.xcodeproj/xcshareddata/xcschemes/Pearvidious (macOS).xcscheme +++ b/Pearvidious.xcodeproj/xcshareddata/xcschemes/Pearvidious (macOS).xcscheme @@ -38,16 +38,6 @@ ReferencedContainer = "container:Pearvidious.xcodeproj"> - - - - ("instances", default: [ .init(app: .piped, id: pipedInstanceID, name: "Public", url: "https://pipedapi.kavin.rocks"), .init(app: .invidious, id: invidiousInstanceID, name: "Private", url: "https://invidious.home.arekf.net") ]) static let accounts = Key<[Account]>("accounts", default: [ - .init(instanceID: invidiousInstanceID, - name: "arekf", - url: "https://invidious.home.arekf.net", - sid: "ki55SJbaQmm0bOxUWctGAQLYPQRgk-CXDPw5Dp4oBmI=") + .init( + id: privateAccountID, + instanceID: invidiousInstanceID, + name: "arekf", + url: "https://invidious.home.arekf.net", + sid: "ki55SJbaQmm0bOxUWctGAQLYPQRgk-CXDPw5Dp4oBmI=" + ) ]) static let lastAccountID = Key("lastAccountID") static let lastInstanceID = Key("lastInstanceID") diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 0db6b794..7757bb09 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -34,6 +34,10 @@ struct ContentView: View { #endif } .onAppear(perform: configure) + + .handlesExternalEvents(preferring: Set(["*"]), allowing: Set(["*"])) + .onOpenURL(perform: handleOpenedURL) + .environmentObject(accounts) .environmentObject(instances) .environmentObject(navigation) @@ -113,6 +117,25 @@ struct ContentView: View { navigation.presentingWelcomeScreen = true } + + func handleOpenedURL(_ url: URL) { + guard !accounts.current.isNil else { + return + } + + let parser = VideoURLParser(url: url) + + guard let id = parser.id else { + return + } + + accounts.api.video(id).load().onSuccess { response in + if let video: Video = response.typedContent() { + self.player.playNow(video, at: parser.time) + self.player.presentPlayer() + } + } + } } struct ContentView_Previews: PreviewProvider { diff --git a/Shared/PearvidiousApp.swift b/Shared/PearvidiousApp.swift index 7f1c41b4..046c599e 100644 --- a/Shared/PearvidiousApp.swift +++ b/Shared/PearvidiousApp.swift @@ -3,10 +3,15 @@ import SwiftUI @main struct PearvidiousApp: App { + #if os(macOS) + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + #endif + var body: some Scene { WindowGroup { ContentView() } + .handlesExternalEvents(matching: Set(["*"])) #if !os(tvOS) .commands { SidebarCommands() diff --git a/Shared/VideoURLParser.swift b/Shared/VideoURLParser.swift new file mode 100644 index 00000000..69500932 --- /dev/null +++ b/Shared/VideoURLParser.swift @@ -0,0 +1,78 @@ +import Foundation + +struct VideoURLParser { + let url: URL + + var id: String? { + if urlComponents?.host == "youtu.be", let path = urlComponents?.path { + return String(path.suffix(from: path.index(path.startIndex, offsetBy: 1))) + } + + return queryItemValue("v") + } + + var time: TimeInterval? { + guard let time = queryItemValue("t") else { + return nil + } + + let timeComponents = parseTime(time) + + guard !timeComponents.isEmpty, + let hours = TimeInterval(timeComponents["hours"] ?? "0"), + let minutes = TimeInterval(timeComponents["minutes"] ?? "0"), + let seconds = TimeInterval(timeComponents["seconds"] ?? "0") + else { + if let time = TimeInterval(time) { + return time + } + + return nil + } + + return seconds + (minutes * 60) + (hours * 60 * 60) + } + + func queryItemValue(_ name: String) -> String? { + queryItems.first { $0.name == name }?.value + } + + private var queryItems: [URLQueryItem] { + urlComponents?.queryItems ?? [] + } + + private var urlComponents: URLComponents? { + URLComponents(url: url, resolvingAgainstBaseURL: false) + } + + private func parseTime(_ time: String) -> [String: String] { + let results = timeRegularExpression.matches( + in: time, + range: NSRange(time.startIndex..., in: time) + ) + + guard let match = results.first else { + return [:] + } + + var components: [String: String] = [:] + + for name in ["hours", "minutes", "seconds"] { + let matchRange = match.range(withName: name) + + if let substringRange = Range(matchRange, in: time) { + let capture = String(time[substringRange]) + components[name] = capture + } + } + + return components + } + + private var timeRegularExpression: NSRegularExpression { + try! NSRegularExpression( + pattern: "(?:(?[0-9+])+h)?(?:(?[0-9]+)m)?(?:(?[0-9]*)s)?", + options: .caseInsensitive + ) + } +} diff --git a/Shared/Videos/HorizontalCells.swift b/Shared/Videos/HorizontalCells.swift index 287b0490..1e798c76 100644 --- a/Shared/Videos/HorizontalCells.swift +++ b/Shared/Videos/HorizontalCells.swift @@ -31,7 +31,6 @@ struct HorizontalCells: View { .padding(.vertical, 10) #endif } - .id(items.map(\.id).joined()) #if os(tvOS) .frame(height: 560) #else diff --git a/Shared/Videos/VerticalCells.swift b/Shared/Videos/VerticalCells.swift index a0aebd0c..58d87691 100644 --- a/Shared/Videos/VerticalCells.swift +++ b/Shared/Videos/VerticalCells.swift @@ -17,7 +17,6 @@ struct VerticalCells: View { } .padding() } - .id(items.map(\.id).joined()) .edgesIgnoringSafeArea(.horizontal) #if os(macOS) .background() diff --git a/Shared Tests/Extensions/Int+FormatTests.swift b/Tests macOS/Extensions/Int+FormatTests.swift similarity index 100% rename from Shared Tests/Extensions/Int+FormatTests.swift rename to Tests macOS/Extensions/Int+FormatTests.swift diff --git a/Tests macOS/Tests_macOS.swift b/Tests macOS/Tests_macOS.swift index ebc75a43..9b7e071d 100644 --- a/Tests macOS/Tests_macOS.swift +++ b/Tests macOS/Tests_macOS.swift @@ -8,28 +8,6 @@ import XCTest class Tests_macOS: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - func testLaunchPerformance() throws { if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { // This measures how long it takes to launch your application. diff --git a/Tests macOS/VideoURLParserTests.swift b/Tests macOS/VideoURLParserTests.swift new file mode 100644 index 00000000..1aa1cedb --- /dev/null +++ b/Tests macOS/VideoURLParserTests.swift @@ -0,0 +1,39 @@ +import XCTest + +final class VideoURLParserTests: XCTestCase { + func testIDParsing() throws { + let samples: [String: String] = [ + "https://www.youtube.com/watch?v=_E0PWQvW-14&list=WL&index=4&t=155s": "_E0PWQvW-14", + "https://youtu.be/IRsc57nK8mg?t=20": "IRsc57nK8mg", + "https://www.youtube-nocookie.com/watch?index=4&v=cE1PSQrWc11&list=WL&t=155s": "cE1PSQrWc11", + "https://invidious.snopyta.org/watch?v=XpowfENlJAw" : "XpowfENlJAw", + "/watch?v=VQ_f5RymW70" : "VQ_f5RymW70", + "watch?v=IUTGFQpKaPU&t=30s": "IUTGFQpKaPU" + ] + + samples.forEach { url, id in + XCTAssertEqual( + VideoURLParser(url: URL(string: url)!).id, + id + ) + } + } + + func testTimeParsing() throws { + let samples: [String: TimeInterval?] = [ + "https://www.youtube.com/watch?v=_E0PWQvW-14&list=WL&index=4&t=155s": 155, + "https://youtu.be/IRsc57nK8mg?t=20m10s": 1210, + "https://youtu.be/IRsc57nK8mg?t=3x4z": nil, + "https://www.youtube-nocookie.com/watch?index=4&v=cE1PSQrWc11&list=WL&t=2H3m5s": 7385, + "https://youtu.be/VQ_f5RymW70?t=378": 378, + "watch?v=IUTGFQpKaPU&t=30s": 30 + ] + + samples.forEach { url, time in + XCTAssertEqual( + VideoURLParser(url: URL(string: url)!).time, + time + ) + } + } +} diff --git a/iOS/Info.plist b/iOS/Info.plist index f753731e..4526445b 100644 --- a/iOS/Info.plist +++ b/iOS/Info.plist @@ -2,6 +2,19 @@ + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + net.yattee.app.watch + CFBundleURLSchemes + + yattee + + + UIBackgroundModes audio diff --git a/macOS/AppDelegate.swift b/macOS/AppDelegate.swift new file mode 100644 index 00000000..b097f46c --- /dev/null +++ b/macOS/AppDelegate.swift @@ -0,0 +1,8 @@ +import AppKit +import Foundation + +final class AppDelegate: NSObject, NSApplicationDelegate { + func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { + true + } +} diff --git a/macOS/Info.plist b/macOS/Info.plist new file mode 100644 index 00000000..e8105311 --- /dev/null +++ b/macOS/Info.plist @@ -0,0 +1,19 @@ + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + net.yattee.app.watch + CFBundleURLSchemes + + yattee + + + + +