react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -21
- package/android/build.gradle +35 -12
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +389 -164
- package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +24 -5
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
- package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
- package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
- package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
- package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
- package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
- package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +56 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
- package/android/src/main/res/values/strings.xml +25 -5
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
- package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
- package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/Browser/BrowserConfig.swift +102 -5
- package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
- package/ios/Browser/BrowserManager.swift +230 -205
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +264 -106
- package/ios/Browser/SearchDrillIn.swift +7 -7
- package/ios/Browser/SectionPresentation.swift +64 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +58 -0
- package/ios/Browser/TabBarEntries.swift +9 -3
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +227 -85
- package/ios/CarPlay/CarPlayImageLoader.swift +42 -20
- package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
- package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
- package/ios/Extension/Section+Copying.swift +29 -0
- package/ios/Extension/String+IcyCharset.swift +21 -0
- package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
- package/ios/Extension/Track+Copying.swift +8 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +253 -100
- package/ios/Model/NitroTypeStubs.swift +125 -23
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/Option/SessionCategories.swift +3 -2
- package/ios/Player/MediaIntentCriteria.swift +17 -9
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +54 -6
- package/ios/Player/PlaybackCoordinator.swift +13 -6
- package/ios/Player/PlaybackErrorHandler.swift +4 -4
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +46 -13
- package/ios/Player/RetryManager.swift +80 -18
- package/ios/Player/SeekCompletionHandler.swift +3 -1
- package/ios/Player/ShuffleOrder.swift +33 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +62 -24
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +150 -1
- package/ios/Tests/LRUCacheTests.swift +111 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
- package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
- package/ios/Tests/QueueManagerTests.swift +122 -10
- package/ios/Tests/RemoteCommandControllerTests.swift +172 -0
- package/ios/Tests/RemoteCommandTests.swift +67 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +90 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +127 -0
- package/ios/Tests/Support/TestTypes.swift +8 -1
- package/ios/Tests/TabBarEntriesTests.swift +19 -19
- package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackPlayer.swift +63 -35
- package/ios/TrackPlayerCallbacks.swift +5 -38
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/ios/Util/LRUCache.swift +4 -1
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- package/lib/commonjs/AudioBrowser.js +8 -8
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser-config.js +12 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +11 -3
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +1 -1
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +3 -3
- package/lib/commonjs/features/favorites.js +14 -11
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/gate.js +16 -1
- package/lib/commonjs/features/gate.js.map +1 -1
- package/lib/commonjs/features/metadata.js +20 -3
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/network.js +1 -1
- package/lib/commonjs/features/nowPlaying.js +1 -1
- package/lib/commonjs/features/output.js +1 -1
- package/lib/commonjs/features/playback/controls.js +4 -2
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/playWhenReady.js +1 -1
- package/lib/commonjs/features/playback/playing.js +1 -1
- package/lib/commonjs/features/playback/progress.js +3 -2
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.js.map +1 -1
- package/lib/commonjs/features/playback/state.js +3 -4
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +48 -34
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +18 -9
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +23 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +15 -4
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +13 -6
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/features/queue/repeatMode.js +1 -1
- package/lib/commonjs/features/queue/shuffle.js +1 -1
- package/lib/commonjs/features/remoteControls.js +13 -92
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +13 -1
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
- package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
- package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/getTrackIdentity.js +24 -0
- package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +200 -75
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
- package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +175 -100
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/SearchManager.js +18 -23
- package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/commonjs/web/browser/sections.js +89 -0
- package/lib/commonjs/web/browser/sections.js.map +1 -0
- package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
- package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
- package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +2 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser-config.js +12 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +11 -3
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +1 -1
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +3 -3
- package/lib/module/features/favorites.js +14 -11
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/gate.js +16 -1
- package/lib/module/features/gate.js.map +1 -1
- package/lib/module/features/metadata.js +20 -3
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/network.js +1 -1
- package/lib/module/features/nowPlaying.js +1 -1
- package/lib/module/features/output.js +1 -1
- package/lib/module/features/playback/controls.js +4 -2
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/playWhenReady.js +1 -1
- package/lib/module/features/playback/playing.js +1 -1
- package/lib/module/features/playback/progress.js +3 -2
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/rate.js +8 -1
- package/lib/module/features/playback/rate.js.map +1 -1
- package/lib/module/features/playback/state.js +3 -4
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +0 -5
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +47 -32
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +17 -8
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +19 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +15 -4
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +13 -6
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/features/queue/repeatMode.js +1 -1
- package/lib/module/features/queue/shuffle.js +1 -1
- package/lib/module/features/remoteControls.js +13 -92
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +13 -1
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/test-utils/reactNativeStub.js +58 -0
- package/lib/module/test-utils/reactNativeStub.js.map +1 -0
- package/lib/module/utils/LazyNativeEmitter.js +28 -14
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +31 -13
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/getTrackIdentity.js +20 -0
- package/lib/module/utils/getTrackIdentity.js.map +1 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +200 -75
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
- package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/module/web/TrackPlayer/Player.js +254 -39
- package/lib/module/web/TrackPlayer/Player.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +175 -100
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js +31 -21
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/SearchManager.js +18 -23
- package/lib/module/web/browser/SearchManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/module/web/browser/sections.js +83 -0
- package/lib/module/web/browser/sections.js.map +1 -0
- package/lib/module/web/http/RequestConfigBuilder.js +110 -27
- package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/module/web/http/followMediaRedirect.js +215 -0
- package/lib/module/web/http/followMediaRedirect.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +56 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/browser.d.ts +10 -2
- package/lib/typescript/src/features/browser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +1 -1
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +3 -3
- package/lib/typescript/src/features/favorites.d.ts +14 -11
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/gate.d.ts +18 -2
- package/lib/typescript/src/features/gate.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +20 -3
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/network.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/output.d.ts +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +4 -2
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
- package/lib/typescript/src/features/playback/playing.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +3 -4
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +108 -56
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +14 -4
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +7 -4
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
- package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +23 -22
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
- package/lib/typescript/src/types/browser-native.d.ts +3 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +380 -126
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +82 -57
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +7 -10
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +45 -25
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +36 -11
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +15 -12
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +7 -10
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +7 -10
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +57 -49
- package/nitrogen/generated/android/c++/JSection.hpp +116 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +25 -46
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +8 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +15 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -12
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +244 -133
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +31 -16
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +33 -18
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
- package/nitrogen/generated/ios/swift/Section.swift +82 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +28 -63
- package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
- package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
- package/nitrogen/generated/shared/c++/Section.hpp +102 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +18 -30
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
- package/package.json +24 -11
- package/src/AudioBrowser.ts +2 -1
- package/src/CLAUDE.md +52 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +12 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +11 -3
- package/src/features/carConnection.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +3 -3
- package/src/features/favorites.ts +14 -11
- package/src/features/gate.ts +16 -1
- package/src/features/hook-wiring.test.ts +302 -0
- package/src/features/metadata.ts +20 -3
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/playWhenReady.ts +1 -1
- package/src/features/playback/playing.ts +1 -1
- package/src/features/playback/progress.test.ts +169 -0
- package/src/features/playback/progress.ts +3 -2
- package/src/features/playback/rate.ts +8 -1
- package/src/features/playback/state.ts +3 -4
- package/src/features/playback/trackPlaybackTime.test.ts +16 -6
- package/src/features/playback/trackPlaybackTime.ts +1 -6
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.ts +126 -76
- package/src/features/player/setup.test.ts +24 -7
- package/src/features/player/setup.ts +22 -10
- package/src/features/player/validateOptions.ts +21 -0
- package/src/features/queue/activeTrack.ts +15 -4
- package/src/features/queue/queue.ts +13 -6
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -92
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +9 -1
- package/src/nitro-stub-drift.test.ts +277 -0
- package/src/public-surface.test.ts +147 -0
- package/src/specs/audio-browser.nitro.ts +5 -4
- package/src/style-resolution.test.ts +210 -0
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/types/browser-native.ts +7 -3
- package/src/types/browser-nodes.ts +396 -134
- package/src/types/browser.ts +85 -59
- package/src/utils/LazyNativeEmitter.ts +28 -15
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
- package/src/web/NativeAudioBrowser.ts +239 -82
- package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
- package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
- package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
- package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
- package/src/web/TrackPlayer/Player.load.test.ts +137 -1
- package/src/web/TrackPlayer/Player.ts +284 -37
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
- package/src/web/__tests__/lifecycle.test.ts +345 -0
- package/src/web/__tests__/lifecycleHarness.ts +351 -0
- package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
- package/src/web/__tests__/loadSupersession.test.ts +175 -0
- package/src/web/__tests__/retry.test.ts +169 -0
- package/src/web/browser/BrowserManager.ts +209 -111
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- package/src/web/browser/SearchManager.test.ts +75 -0
- package/src/web/browser/SearchManager.ts +25 -28
- package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
- package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
- package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
- package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
- package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
- package/src/web/browser/assertBrowsePageShape.ts +5 -3
- package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
- package/src/web/browser/classifyTrackNavigation.ts +9 -9
- package/src/web/browser/parseSearchResponse.test.ts +8 -2
- package/src/web/browser/sections.test.ts +91 -0
- package/src/web/browser/sections.ts +72 -0
- package/src/web/http/RequestConfigBuilder.test.ts +241 -1
- package/src/web/http/RequestConfigBuilder.ts +140 -33
- package/src/web/http/followMediaRedirect.test.ts +289 -0
- package/src/web/http/followMediaRedirect.ts +253 -0
- package/src/web/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- package/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +581 -0
- package/website/guide/carplay.md +620 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +247 -0
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/ios/Model/MediaURL.swift +0 -67
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
- package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
|
@@ -64,14 +64,11 @@ final class BrowserManager {
|
|
|
64
64
|
private let router = SimpleRouter()
|
|
65
65
|
private let httpClient = HttpClient()
|
|
66
66
|
|
|
67
|
-
// LRU cache for individual tracks - keyed by both url and src for O(1) lookup
|
|
68
|
-
private let trackCache = LRUCache<String, Track>(maxSize: 3000)
|
|
69
|
-
|
|
70
67
|
// LRU cache for resolved content - keyed by path
|
|
71
68
|
private let contentCache = LRUCache<String, ResolvedTrack>(maxSize: 20)
|
|
72
69
|
|
|
73
|
-
//
|
|
74
|
-
private var
|
|
70
|
+
// Most recent search, keyed by `searchCacheKey`.
|
|
71
|
+
private var lastSearchKey: String?
|
|
75
72
|
private var lastSearchResults: [Track]?
|
|
76
73
|
|
|
77
74
|
// Resolver caching: request/browse thunks resolve once per content generation.
|
|
@@ -80,12 +77,12 @@ final class BrowserManager {
|
|
|
80
77
|
var resolvedRequestLayer: TransformableRequestConfig?
|
|
81
78
|
private var resolvedBrowseLayer: TransformableRequestConfig?
|
|
82
79
|
|
|
83
|
-
// Set of favorited track
|
|
80
|
+
// Set of favorited track identities (id when non-blank, else src).
|
|
84
81
|
private var favoriteIds: Set<String> = []
|
|
85
82
|
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
private var
|
|
83
|
+
// Whether favoriting is enabled, propagated from the player's `favorite`
|
|
84
|
+
// capability. false = no row hearts. Set via setFavoriteEnabled.
|
|
85
|
+
private var favoriteEnabled = false
|
|
89
86
|
|
|
90
87
|
// Navigation tracking to prevent race conditions
|
|
91
88
|
private var currentNavigationId: Int = 0
|
|
@@ -165,78 +162,46 @@ final class BrowserManager {
|
|
|
165
162
|
onFavoritesChanged?()
|
|
166
163
|
}
|
|
167
164
|
|
|
168
|
-
///
|
|
169
|
-
///
|
|
170
|
-
func
|
|
171
|
-
|
|
165
|
+
/// Enables/disables favoriting (propagated from the `favorite` capability).
|
|
166
|
+
/// false disables row-heart hydration.
|
|
167
|
+
func setFavoriteEnabled(_ enabled: Bool) {
|
|
168
|
+
favoriteEnabled = enabled
|
|
172
169
|
}
|
|
173
170
|
|
|
174
|
-
/// Optimistically reflects a local favorite toggle
|
|
175
|
-
///
|
|
176
|
-
///
|
|
177
|
-
///
|
|
178
|
-
///
|
|
179
|
-
|
|
180
|
-
/// aren't collaterally lost. The consumer reconciles `favoriteIds` to its
|
|
181
|
-
/// canonical ids on the next `setFavorites`; `isFavorite(src:)` gives the same
|
|
182
|
-
/// answer before and after, so the now-playing heart stays responsive.
|
|
183
|
-
func setFavorited(src: String, favorited: Bool) {
|
|
171
|
+
/// Optimistically reflects a local favorite toggle in the favorite set, keyed
|
|
172
|
+
/// by the track's identity (id when non-blank, else src). The consumer
|
|
173
|
+
/// reconciles `favoriteIds` to its canonical set on the next `setFavorites`;
|
|
174
|
+
/// `hydrateFavorite` gives the same answer before and after, so the
|
|
175
|
+
/// now-playing heart stays responsive.
|
|
176
|
+
func setFavorited(identity: String, favorited: Bool) {
|
|
184
177
|
if favorited {
|
|
185
|
-
favoriteIds.insert(
|
|
186
|
-
} else if favoriteMatch == .partial {
|
|
187
|
-
favoriteIds = favoriteIds.filter { !BrowserPathHelper.containsSegment(src, $0) }
|
|
178
|
+
favoriteIds.insert(identity)
|
|
188
179
|
} else {
|
|
189
|
-
favoriteIds.remove(
|
|
180
|
+
favoriteIds.remove(identity)
|
|
190
181
|
}
|
|
191
182
|
}
|
|
192
183
|
|
|
193
184
|
/// Hydrates the favorited field on a track based on the favoriteIds set.
|
|
194
185
|
/// No-op unless favoriting is enabled (the `favorite` capability). Only
|
|
195
|
-
///
|
|
196
|
-
/// false so non-favorited tracks still show an (empty) heart.
|
|
197
|
-
///
|
|
186
|
+
/// identity-bearing tracks (id or src) are favoritable; the flag is set to
|
|
187
|
+
/// true OR false so non-favorited tracks still show an (empty) heart. A
|
|
188
|
+
/// caller-set `favorited` wins — API/consumer-provided values are never
|
|
189
|
+
/// overwritten (matches Android and web).
|
|
198
190
|
func hydrateFavorite(_ track: Track) -> Track {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
let
|
|
202
|
-
|
|
203
|
-
// Only create a new track if the favorited state differs
|
|
204
|
-
if track.favorited == isFavorited { return track }
|
|
205
|
-
|
|
206
|
-
return track.copying(favorited: isFavorited)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/// Whether `src` is favorited under the given match mode.
|
|
210
|
-
private func isFavorite(src: String, match: FavoritesMatchMode) -> Bool {
|
|
211
|
-
switch match {
|
|
212
|
-
case .exact:
|
|
213
|
-
favoriteIds.contains(src)
|
|
214
|
-
case .partial:
|
|
215
|
-
favoriteIds.contains { BrowserPathHelper.containsSegment(src, $0) }
|
|
216
|
-
}
|
|
191
|
+
// `identity` bridges a std::string out of the C++ track, so it is bound
|
|
192
|
+
// once here rather than computed again to answer "is it favorited".
|
|
193
|
+
guard favoriteEnabled, track.favorited == nil, let identity = track.identity else { return track }
|
|
194
|
+
return track.copying(favorited: favoriteIds.contains(identity))
|
|
217
195
|
}
|
|
218
196
|
|
|
219
|
-
/// Hydrates favorites on all
|
|
197
|
+
/// Hydrates favorites on all tracks of a resolved page. Pages reach
|
|
198
|
+
/// hydration normalized (ADR 0010), so `sections` is the only structure
|
|
199
|
+
/// to walk.
|
|
220
200
|
private func hydrateChildren(_ resolvedTrack: ResolvedTrack) -> ResolvedTrack {
|
|
221
|
-
guard let
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// MARK: - Track Cache
|
|
227
|
-
|
|
228
|
-
/// Cache a track by both url and src for O(1) lookup from either key.
|
|
229
|
-
private func cacheTrack(_ track: Track) {
|
|
230
|
-
if let url = track.url {
|
|
231
|
-
trackCache.set(url, value: track)
|
|
232
|
-
}
|
|
233
|
-
if let src = track.src {
|
|
234
|
-
trackCache.set(src, value: track)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private func cacheChildren(_ resolvedTrack: ResolvedTrack) {
|
|
239
|
-
resolvedTrack.children?.forEach { cacheTrack($0) }
|
|
201
|
+
guard let sections = resolvedTrack.sections else { return resolvedTrack }
|
|
202
|
+
return resolvedTrack.copying(sections: sections.map { section in
|
|
203
|
+
section.copying(children: section.children.map { hydrateFavorite($0) })
|
|
204
|
+
})
|
|
240
205
|
}
|
|
241
206
|
|
|
242
207
|
// MARK: - Navigation
|
|
@@ -253,12 +218,21 @@ final class BrowserManager {
|
|
|
253
218
|
self.path = path
|
|
254
219
|
content = nil // Clear for loading state
|
|
255
220
|
|
|
256
|
-
|
|
221
|
+
do {
|
|
222
|
+
let resolved = try await resolve(path)
|
|
257
223
|
|
|
258
|
-
|
|
259
|
-
|
|
224
|
+
// Only apply result if this is still the current navigation
|
|
225
|
+
guard navigationId == currentNavigationId else { return }
|
|
260
226
|
|
|
261
|
-
|
|
227
|
+
content = resolved
|
|
228
|
+
} catch {
|
|
229
|
+
// A superseded navigation's failure is not the current page's failure.
|
|
230
|
+
// Every caller reports a throw unconditionally (and CarPlay presents it as
|
|
231
|
+
// a modal action sheet), so without this a slow /a that fails after a fast
|
|
232
|
+
// /b succeeded would show /a's error over /b's content.
|
|
233
|
+
guard navigationId == currentNavigationId else { return }
|
|
234
|
+
throw error
|
|
235
|
+
}
|
|
262
236
|
}
|
|
263
237
|
|
|
264
238
|
/// Resolves content for a path with optional caching.
|
|
@@ -276,14 +250,22 @@ final class BrowserManager {
|
|
|
276
250
|
// Cache the resolved content for future navigation
|
|
277
251
|
contentCache.set(normalizedPath, value: resolvedTrack)
|
|
278
252
|
|
|
279
|
-
// Cache children for track lookups
|
|
280
|
-
cacheChildren(resolvedTrack)
|
|
281
|
-
|
|
282
253
|
return hydrateChildren(resolvedTrack)
|
|
283
254
|
}
|
|
284
255
|
|
|
285
256
|
/// Invalidates the content cache for a specific path.
|
|
286
|
-
|
|
257
|
+
///
|
|
258
|
+
/// Expects a *container* path. A contextual path (`…?__trackId=…`) addresses
|
|
259
|
+
/// a child, not a cache entry — entries are keyed by the stripped parent — so
|
|
260
|
+
/// passing one used to remove nothing at all, silently. Android `require`s the
|
|
261
|
+
/// same precondition; this throws so the misuse surfaces identically on both
|
|
262
|
+
/// platforms instead of no-opping on one.
|
|
263
|
+
func invalidateContentCache(_ path: String) throws {
|
|
264
|
+
guard !BrowserPathHelper.isContextual(path) else {
|
|
265
|
+
throw BrowserError.invalidConfiguration(
|
|
266
|
+
"invalidateContentCache() expects a container path, not a contextual path: \(path)",
|
|
267
|
+
)
|
|
268
|
+
}
|
|
287
269
|
contentCache.remove(path)
|
|
288
270
|
}
|
|
289
271
|
|
|
@@ -292,6 +274,11 @@ final class BrowserManager {
|
|
|
292
274
|
func clearContentCache() {
|
|
293
275
|
contentCache.clear()
|
|
294
276
|
layerGeneration += 1
|
|
277
|
+
// The search slot is content too: a locale switch or a re-auth changes
|
|
278
|
+
// titles, src, artwork and availability, and nothing else ever evicts it,
|
|
279
|
+
// so a repeated query returned pre-invalidation results forever.
|
|
280
|
+
lastSearchKey = nil
|
|
281
|
+
lastSearchResults = nil
|
|
295
282
|
}
|
|
296
283
|
|
|
297
284
|
/// Refreshes the current path by invalidating cache and re-resolving.
|
|
@@ -304,12 +291,19 @@ final class BrowserManager {
|
|
|
304
291
|
|
|
305
292
|
let currentPath = path
|
|
306
293
|
contentCache.remove(currentPath)
|
|
307
|
-
let resolved = try await resolve(currentPath, useCache: false)
|
|
308
294
|
|
|
309
|
-
|
|
310
|
-
|
|
295
|
+
do {
|
|
296
|
+
let resolved = try await resolve(currentPath, useCache: false)
|
|
311
297
|
|
|
312
|
-
|
|
298
|
+
// Only apply result if this is still the current navigation
|
|
299
|
+
guard navigationId == currentNavigationId else { return }
|
|
300
|
+
|
|
301
|
+
content = resolved
|
|
302
|
+
} catch {
|
|
303
|
+
// Superseded refreshes stay silent, for the same reason as `navigate`.
|
|
304
|
+
guard navigationId == currentNavigationId else { return }
|
|
305
|
+
throw error
|
|
306
|
+
}
|
|
313
307
|
}
|
|
314
308
|
|
|
315
309
|
private func resolveUncached(_ path: String) async throws -> ResolvedTrack {
|
|
@@ -326,12 +320,18 @@ final class BrowserManager {
|
|
|
326
320
|
)
|
|
327
321
|
}
|
|
328
322
|
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
323
|
+
// Normalize to the canonical sectioned shape (children sugar → one
|
|
324
|
+
// untitled section — ADR 0010), then validate and transform. The output
|
|
325
|
+
// never carries `children`.
|
|
326
|
+
if let sections = resolvedTrack.normalizedSections {
|
|
327
|
+
// Dev diagnostic (ADR 0011) — before the blocks are folded, while each
|
|
328
|
+
// level is still attributable. Every page passes here whatever its
|
|
329
|
+
// source, and only on a cache miss. No-op outside debug builds.
|
|
330
|
+
InertStyleDiagnostic.warn(path: path, pageStyle: resolvedTrack.style, sections: sections)
|
|
331
|
+
let transformed = try await transformSections(
|
|
332
|
+
sections, parentPath: path, routeEntry: routeEntry?.0,
|
|
333
333
|
)
|
|
334
|
-
return resolvedTrack.copying(children:
|
|
334
|
+
return resolvedTrack.copying(sections: transformed, children: .some(nil))
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
return resolvedTrack
|
|
@@ -448,7 +448,7 @@ final class BrowserManager {
|
|
|
448
448
|
)
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
/// Resolves a browse route: a page object (`{title,
|
|
451
|
+
/// Resolves a browse route: a page object (`{title,path,children:[…]}`).
|
|
452
452
|
/// The browse kind layer is the cached resolved layer (`resolvedBrowseLayer`),
|
|
453
453
|
/// populated by `ensureLayersResolved()` inside `buildApiRequest`.
|
|
454
454
|
private func resolveFromConfig(
|
|
@@ -469,8 +469,8 @@ final class BrowserManager {
|
|
|
469
469
|
let nitroResult = result.toNitro()
|
|
470
470
|
|
|
471
471
|
logger.debug("Resolved: \(nitroResult.title)")
|
|
472
|
-
if let
|
|
473
|
-
logger.debug(" children: \(children.count) tracks")
|
|
472
|
+
if let sections = nitroResult.normalizedSections {
|
|
473
|
+
logger.debug(" children: \(sections.reduce(0) { $0 + $1.children.count }) tracks")
|
|
474
474
|
}
|
|
475
475
|
|
|
476
476
|
return nitroResult
|
|
@@ -547,65 +547,51 @@ final class BrowserManager {
|
|
|
547
547
|
|
|
548
548
|
// MARK: - Child Transformation
|
|
549
549
|
|
|
550
|
-
private func
|
|
551
|
-
_
|
|
550
|
+
private func transformSections(
|
|
551
|
+
_ sections: [Section],
|
|
552
552
|
parentPath: String,
|
|
553
553
|
routeEntry: NativeRouteEntry?,
|
|
554
|
-
) async throws -> [
|
|
555
|
-
var transformed: [
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
554
|
+
) async throws -> [Section] {
|
|
555
|
+
var transformed: [Section] = []
|
|
556
|
+
// The contextual index is the flat page position — children concatenated
|
|
557
|
+
// in section order (ADR 0009/0010).
|
|
558
|
+
var flatIndex = 0
|
|
559
|
+
|
|
560
|
+
for section in sections {
|
|
561
|
+
var transformedChildren: [Track] = []
|
|
562
|
+
|
|
563
|
+
for track in section.children {
|
|
564
|
+
let index = flatIndex
|
|
565
|
+
flatIndex += 1
|
|
566
|
+
|
|
567
|
+
// Validate track has stable identifier.
|
|
568
|
+
if track.path == nil, track.src == nil {
|
|
569
|
+
throw BrowserError.invalidConfiguration(
|
|
570
|
+
"Track must have either 'path' or 'src' for stable identification: \(track.title)",
|
|
571
|
+
)
|
|
572
|
+
}
|
|
566
573
|
|
|
567
|
-
|
|
574
|
+
var transformedTrack = track
|
|
568
575
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
transformedTrack = transformedTrack.copying(artworkSource: imageSource)
|
|
578
|
-
}
|
|
576
|
+
if track.src != nil, track.path == nil {
|
|
577
|
+
// src != nil guarantees a non-nil identity (id when non-blank, else src).
|
|
578
|
+
// The flat page position rides along as a duplicate-identity tie-breaker.
|
|
579
|
+
let contextualPath = BrowserPathHelper.build(
|
|
580
|
+
parentPath: parentPath, trackId: track.identity!, index: index,
|
|
581
|
+
)
|
|
582
|
+
transformedTrack = track.copying(path: contextualPath)
|
|
583
|
+
}
|
|
579
584
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
let itemImageSource = await resolveArtworkUrl(track: item.toTrack(), perRouteConfig: artworkConfig)
|
|
585
|
-
// Playable items get a contextual URL like any list row, so a
|
|
586
|
-
// thumbnail tap expands into its section's queue instead of a
|
|
587
|
-
// queue of one (ADR 0006).
|
|
588
|
-
let itemUrl = item.url ?? item.src.map {
|
|
589
|
-
BrowserPathHelper.build(parentPath: parentPath, trackId: $0)
|
|
590
|
-
}
|
|
591
|
-
resolvedItems.append(ImageRowItem(
|
|
592
|
-
id: item.id,
|
|
593
|
-
url: itemUrl,
|
|
594
|
-
src: item.src,
|
|
595
|
-
artwork: item.artwork,
|
|
596
|
-
artworkSource: itemImageSource,
|
|
597
|
-
title: item.title,
|
|
598
|
-
artist: item.artist,
|
|
599
|
-
album: item.album,
|
|
600
|
-
albumUrl: item.albumUrl,
|
|
601
|
-
live: item.live,
|
|
602
|
-
request: item.request,
|
|
603
|
-
))
|
|
585
|
+
// Resolve artwork URL at browse-time (no size context)
|
|
586
|
+
let artworkConfig = routeEntry?.artwork ?? config.artwork
|
|
587
|
+
if let imageSource = await resolveArtworkUrl(track: transformedTrack, perRouteConfig: artworkConfig) {
|
|
588
|
+
transformedTrack = transformedTrack.copying(artworkSource: imageSource)
|
|
604
589
|
}
|
|
605
|
-
|
|
590
|
+
|
|
591
|
+
transformedChildren.append(transformedTrack)
|
|
606
592
|
}
|
|
607
593
|
|
|
608
|
-
transformed.append(
|
|
594
|
+
transformed.append(section.copying(children: transformedChildren))
|
|
609
595
|
}
|
|
610
596
|
|
|
611
597
|
return transformed
|
|
@@ -621,16 +607,49 @@ final class BrowserManager {
|
|
|
621
607
|
)
|
|
622
608
|
}
|
|
623
609
|
|
|
624
|
-
///
|
|
625
|
-
///
|
|
626
|
-
///
|
|
610
|
+
/// The query params a structured search sends: `q` plus whichever filters are
|
|
611
|
+
/// set. Mirrors Android's `executeSearchApiRequest` and the web stub.
|
|
612
|
+
///
|
|
613
|
+
/// Single source of truth for "what distinguishes one search from another" —
|
|
614
|
+
/// the request sends exactly this, and the cache keys on exactly this, so a
|
|
615
|
+
/// field added here can't be forgotten in one place and silently serve one
|
|
616
|
+
/// search's results for another.
|
|
617
|
+
private static func searchQueryParams(_ params: SearchParams) -> [String: String] {
|
|
618
|
+
var query: [String: String] = ["q": params.query]
|
|
619
|
+
if let mode = params.mode { query["mode"] = mode.stringValue }
|
|
620
|
+
if params.reference == .my { query["reference"] = "my" }
|
|
621
|
+
if let genre = params.genre { query["genre"] = genre }
|
|
622
|
+
if let artist = params.artist { query["artist"] = artist }
|
|
623
|
+
if let album = params.album { query["album"] = album }
|
|
624
|
+
if let title = params.title { query["title"] = title }
|
|
625
|
+
if let playlist = params.playlist { query["playlist"] = playlist }
|
|
626
|
+
return query
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/// The cache identity of a search. Sorted so key order is stable, and
|
|
630
|
+
/// `\u{1}`-delimited so two different param sets can't serialize alike.
|
|
631
|
+
private static func searchCacheKey(_ params: SearchParams) -> String {
|
|
632
|
+
searchQueryParams(params)
|
|
633
|
+
.sorted { $0.key < $1.key }
|
|
634
|
+
.map { "\($0.key)=\($0.value)" }
|
|
635
|
+
.joined(separator: "\u{1}")
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/// Structured search, cached by the full parameter set.
|
|
639
|
+
///
|
|
640
|
+
/// Voice intents reuse `query` across different structures —
|
|
641
|
+
/// `MediaIntentCriteria` falls back to the structured value when there is no
|
|
642
|
+
/// spoken name, so "play jazz" (genre) and a text search for "jazz" arrive
|
|
643
|
+
/// with an identical `query` — hence `searchCacheKey` rather than `query`.
|
|
627
644
|
func search(_ params: SearchParams) async throws -> ResolvedTrack {
|
|
628
645
|
let query = params.query
|
|
646
|
+
let cacheKey = Self.searchCacheKey(params)
|
|
629
647
|
|
|
630
|
-
// Check cache -
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
648
|
+
// Check cache - hydrate favorites on the way out, since they may have
|
|
649
|
+
// changed since the search ran (the slot holds the raw results for exactly
|
|
650
|
+
// this reason — see below).
|
|
651
|
+
if cacheKey == lastSearchKey, let results = lastSearchResults {
|
|
652
|
+
return makeSearchResult(query: query, results: results)
|
|
634
653
|
}
|
|
635
654
|
|
|
636
655
|
guard let routes = config.routes else {
|
|
@@ -655,18 +674,9 @@ final class BrowserManager {
|
|
|
655
674
|
// Seed the search config's static path onto the base — a layer's static
|
|
656
675
|
// path never applies (carried from the base; only a transform may change
|
|
657
676
|
// it). Matches the web stub's fetchSearchResults and Android.
|
|
658
|
-
var queryParams: [String: String] = ["q": query]
|
|
659
|
-
if let mode = params.mode { queryParams["mode"] = mode.stringValue }
|
|
660
|
-
if params.reference == .my { queryParams["reference"] = "my" }
|
|
661
|
-
if let genre = params.genre { queryParams["genre"] = genre }
|
|
662
|
-
if let artist = params.artist { queryParams["artist"] = artist }
|
|
663
|
-
if let album = params.album { queryParams["album"] = album }
|
|
664
|
-
if let title = params.title { queryParams["title"] = title }
|
|
665
|
-
if let playlist = params.playlist { queryParams["playlist"] = playlist }
|
|
666
|
-
|
|
667
677
|
let request = try await buildApiRequest(
|
|
668
678
|
kind: nil, searchConfig, path: searchConfig.path ?? "", params: ["q": query],
|
|
669
|
-
initialQuery:
|
|
679
|
+
initialQuery: Self.searchQueryParams(params),
|
|
670
680
|
)
|
|
671
681
|
logger.debug("Searching via API: \(request.url)")
|
|
672
682
|
let jsonTracks: [JsonTrack] = try await httpClient.requestJson(request, as: [JsonTrack].self)
|
|
@@ -675,17 +685,15 @@ final class BrowserManager {
|
|
|
675
685
|
throw BrowserError.contentNotFound(path: Self.searchRoutePath)
|
|
676
686
|
}
|
|
677
687
|
|
|
678
|
-
//
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
// Cache individual tracks
|
|
686
|
-
hydratedResults.forEach { cacheTrack($0) }
|
|
688
|
+
// Cache the RAW results, never the hydrated ones: hydration is one-shot
|
|
689
|
+
// (`hydrateFavorite` skips a track whose `favorited` is already set), so a
|
|
690
|
+
// hydrated slot would serve the favorites as they were at first search
|
|
691
|
+
// forever. The content cache keys on the same rule — it stores the
|
|
692
|
+
// unhydrated resolve and hydrates in `resolve()`.
|
|
693
|
+
lastSearchKey = cacheKey
|
|
694
|
+
lastSearchResults = results
|
|
687
695
|
|
|
688
|
-
return makeSearchResult(query: query, results:
|
|
696
|
+
return makeSearchResult(query: query, results: results)
|
|
689
697
|
}
|
|
690
698
|
|
|
691
699
|
/// Search and return playable tracks for "play «X»" voice intents, drilling
|
|
@@ -693,37 +701,44 @@ final class BrowserManager {
|
|
|
693
701
|
/// station. Decision in `SearchDrillIn.playable`; mirrors Android's
|
|
694
702
|
/// `searchPlayable`.
|
|
695
703
|
func searchPlayable(_ params: SearchParams) async throws -> [Track]? {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
704
|
+
// Voice search never matches a disabled track (Track.disabled) — filtered
|
|
705
|
+
// before drill-in so an unavailable first result can't capture the play.
|
|
706
|
+
let children = try await ((search(params).flattenedChildren) ?? [])
|
|
707
|
+
.filter { $0.disabled != true }
|
|
708
|
+
return try await SearchDrillIn.playable(from: children) { [self] path in
|
|
709
|
+
logger.debug("First search result is browsable-only, resolving: \(path)")
|
|
710
|
+
return try await ((resolve(path).flattenedChildren) ?? []).filter { $0.disabled != true }
|
|
700
711
|
}
|
|
701
712
|
}
|
|
702
713
|
|
|
714
|
+
/// Wraps raw search results as a resolved page, hydrating favorites on the
|
|
715
|
+
/// way out — the single exit for `search`, so an unhydrated search page can't
|
|
716
|
+
/// be emitted (the same funnel `resolve()` gets from `hydrateChildren`).
|
|
703
717
|
private func makeSearchResult(query: String, results: [Track]) -> ResolvedTrack {
|
|
718
|
+
// Search results are a flat list; as a resolved page they are one
|
|
719
|
+
// untitled section (ADR 0010).
|
|
704
720
|
ResolvedTrack(
|
|
705
|
-
|
|
706
|
-
|
|
721
|
+
path: BrowserPathHelper.createSearchPath(query),
|
|
722
|
+
style: nil,
|
|
723
|
+
sections: [.untitled(results.map { hydrateFavorite($0) })],
|
|
724
|
+
children: nil,
|
|
707
725
|
carPlaySiriListButton: nil,
|
|
708
726
|
id: nil,
|
|
709
727
|
src: nil,
|
|
710
728
|
artwork: nil,
|
|
711
729
|
artworkSource: nil, request: nil,
|
|
712
|
-
artworkCarPlayTinted: nil,
|
|
713
730
|
title: "Search: \(query)",
|
|
714
731
|
subtitle: nil,
|
|
715
732
|
artist: nil,
|
|
716
|
-
|
|
733
|
+
albumPath: nil,
|
|
717
734
|
album: nil,
|
|
718
735
|
description: nil,
|
|
719
736
|
genre: nil,
|
|
720
737
|
duration: nil,
|
|
721
|
-
|
|
722
|
-
childrenStyle: nil,
|
|
738
|
+
disabled: nil,
|
|
723
739
|
favorited: nil,
|
|
724
|
-
groupTitle: nil,
|
|
725
740
|
live: nil,
|
|
726
|
-
|
|
741
|
+
icyCharset: nil,
|
|
727
742
|
)
|
|
728
743
|
}
|
|
729
744
|
|
|
@@ -740,7 +755,8 @@ final class BrowserManager {
|
|
|
740
755
|
}
|
|
741
756
|
|
|
742
757
|
let resolved = try await resolveRouteEntry(tabsEntry, path: Self.tabsRoutePath, params: [:])
|
|
743
|
-
|
|
758
|
+
// Tabs are a flat list, not a page — a sectioned tabs source flattens.
|
|
759
|
+
let tabTracks = resolved.flattenedChildren ?? []
|
|
744
760
|
|
|
745
761
|
tabs = tabTracks
|
|
746
762
|
return tabTracks
|
|
@@ -748,38 +764,39 @@ final class BrowserManager {
|
|
|
748
764
|
|
|
749
765
|
// MARK: - Queue Expansion (for CarPlay/external controllers)
|
|
750
766
|
|
|
751
|
-
/// Expands a contextual
|
|
752
|
-
func
|
|
753
|
-
guard BrowserPathHelper.isContextual(
|
|
767
|
+
/// Expands a contextual path to a full queue of playable tracks.
|
|
768
|
+
func expandQueueFromContextualPath(_ path: String) async throws -> (tracks: [Track], selectedIndex: Int)? {
|
|
769
|
+
guard BrowserPathHelper.isContextual(path) else { return nil }
|
|
754
770
|
|
|
755
|
-
let parentPath = BrowserPathHelper.stripTrackId(
|
|
756
|
-
guard let trackId = BrowserPathHelper.extractTrackId(
|
|
771
|
+
let parentPath = BrowserPathHelper.stripTrackId(path)
|
|
772
|
+
guard let trackId = BrowserPathHelper.extractTrackId(path) else { return nil }
|
|
757
773
|
|
|
758
|
-
logger.debug("Expanding queue from contextual
|
|
759
|
-
logger.debug("
|
|
774
|
+
logger.debug("Expanding queue from contextual path")
|
|
775
|
+
logger.debug(" path: \(path)")
|
|
760
776
|
logger.debug(" parentPath: \(parentPath)")
|
|
761
777
|
logger.debug(" trackId: \(trackId)")
|
|
762
778
|
|
|
763
779
|
// Resolve the parent container
|
|
764
780
|
let resolved = try await resolve(parentPath, useCache: true)
|
|
765
|
-
guard let
|
|
766
|
-
|
|
767
|
-
// Queue scope is the tapped section, not the whole page (ADR 0006).
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
sectionTracks = tracks
|
|
777
|
-
case nil:
|
|
781
|
+
guard let sections = resolved.normalizedSections else { return nil }
|
|
782
|
+
|
|
783
|
+
// Queue scope is the tapped section, not the whole page (ADR 0006). The
|
|
784
|
+
// stamped flat index pins which section (and which copy) was tapped when
|
|
785
|
+
// the same identity appears more than once; it is only a tie-breaker —
|
|
786
|
+
// a stale index falls back to the first identity match. An id that no
|
|
787
|
+
// longer appears on the page at all aborts the expansion — the caller
|
|
788
|
+
// falls back to the stored single track (matching Android); silently
|
|
789
|
+
// queueing the changed list would resume the wrong station.
|
|
790
|
+
let tappedIndex = BrowserPathHelper.extractIndex(path)
|
|
791
|
+
guard let scoped = SectionScope.scoped(in: sections, containing: trackId, tappedIndex: tappedIndex) else {
|
|
778
792
|
return nil
|
|
779
793
|
}
|
|
794
|
+
let sectionTracks = scoped.section.children
|
|
795
|
+
let tappedOffset = scoped.tappedOffset
|
|
780
796
|
|
|
781
|
-
// Filter to playable tracks (have src)
|
|
782
|
-
|
|
797
|
+
// Filter to playable tracks (have src). A disabled track is unavailable —
|
|
798
|
+
// queue expansion excludes it, so auto-advance never meets one.
|
|
799
|
+
let playableTracks = sectionTracks.filter { $0.src != nil && $0.disabled != true }
|
|
783
800
|
guard !playableTracks.isEmpty else { return nil }
|
|
784
801
|
|
|
785
802
|
logger.debug("Found \(playableTracks.count) playable tracks")
|
|
@@ -787,10 +804,18 @@ final class BrowserManager {
|
|
|
787
804
|
logger.debug(" [\(index)] \(track.title) - src: \(track.src ?? "nil")")
|
|
788
805
|
}
|
|
789
806
|
|
|
790
|
-
// Find selected track index
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
|
|
807
|
+
// Find selected track index: the pinned copy when the stamp survived,
|
|
808
|
+
// else the first identity match. A miss must abort rather than silently
|
|
809
|
+
// start the queue at the wrong track (unreachable in practice — the
|
|
810
|
+
// section was found BY this id).
|
|
811
|
+
let selectedIndex: Int
|
|
812
|
+
if let tappedOffset, sectionTracks[tappedOffset].src != nil, sectionTracks[tappedOffset].disabled != true {
|
|
813
|
+
selectedIndex =
|
|
814
|
+
sectionTracks.prefix(through: tappedOffset)
|
|
815
|
+
.count(where: { $0.src != nil && $0.disabled != true }) - 1
|
|
816
|
+
} else if let index = playableTracks.firstIndex(where: { $0.identity == trackId }) {
|
|
817
|
+
selectedIndex = index
|
|
818
|
+
} else {
|
|
794
819
|
return nil
|
|
795
820
|
}
|
|
796
821
|
logger.debug("Selected track index: \(selectedIndex)")
|