react-native-audio-browser 0.2.0-next.11 → 0.2.0-next.13
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 +14 -4
- package/android/build.gradle +20 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +115 -26
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +262 -188
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +95 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +19 -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/model/PlayerUpdateOptions.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +109 -74
- package/android/src/main/java/com/audiobrowser/player/Player.kt +51 -30
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -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 +52 -53
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +74 -41
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +229 -50
- 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 +1 -1
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
- 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/player/CapabilityControlsTest.kt +1 -2
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -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 +229 -180
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +258 -106
- package/ios/Browser/SearchDrillIn.swift +6 -6
- 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 +38 -18
- package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
- package/ios/Extension/Section+Copying.swift +31 -0
- package/ios/Extension/Track+Copying.swift +6 -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 +166 -45
- package/ios/Model/NitroTypeStubs.swift +106 -21
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Player/MediaIntentCriteria.swift +13 -5
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +54 -6
- package/ios/Player/PlaybackCoordinator.swift +13 -6
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +36 -10
- package/ios/Player/RetryManager.swift +4 -3
- package/ios/Player/ShuffleOrder.swift +23 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
- 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/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/QueueManagerTests.swift +72 -6
- package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
- 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 +7 -0
- package/ios/Tests/TabBarEntriesTests.swift +18 -18
- 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 +57 -34
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -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 +11 -0
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/features/browser-config.js +11 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +2 -2
- package/lib/commonjs/features/favorites.js +13 -10
- package/lib/commonjs/features/favorites.js.map +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/progress.js +2 -1
- 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 +2 -3
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +3 -1
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +2 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -1
- package/lib/commonjs/features/queue/activeTrack.js +14 -3
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +11 -4
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
- 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 +32 -24
- package/lib/commonjs/web/NativeAudioBrowser.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/browser/BrowserManager.js +144 -87
- 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/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/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 +1 -0
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/features/browser-config.js +11 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +2 -2
- package/lib/module/features/favorites.js +13 -10
- package/lib/module/features/favorites.js.map +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/progress.js +2 -1
- 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 +2 -3
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +3 -1
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +2 -0
- package/lib/module/features/player/validateOptions.js.map +1 -1
- package/lib/module/features/queue/activeTrack.js +14 -3
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +11 -4
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/utils/LazyNativeEmitter.js +21 -10
- 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 +32 -24
- package/lib/module/web/NativeAudioBrowser.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/browser/BrowserManager.js +144 -87
- 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/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/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/src/AudioBrowser.d.ts +1 -0
- 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 +2 -2
- package/lib/typescript/src/features/favorites.d.ts +13 -10
- package/lib/typescript/src/features/favorites.d.ts.map +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/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 +2 -3
- 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/player/options.d.ts +21 -19
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +5 -2
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +361 -126
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +33 -53
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
- 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 +0 -1
- 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__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 +2 -5
- 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 +33 -14
- 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 +22 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +10 -7
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +55 -51
- package/nitrogen/generated/android/c++/JSection.hpp +120 -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 +23 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
- 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/GridTile.kt +24 -0
- 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 +5 -5
- 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/ResolvedTrack.kt +12 -18
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +18 -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 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +158 -82
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +25 -10
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +25 -10
- 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/GridTile.swift +44 -0
- 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 +5 -5
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
- package/nitrogen/generated/ios/swift/Section.swift +100 -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 +20 -73
- package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
- 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++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
- package/nitrogen/generated/shared/c++/Section.hpp +106 -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 +15 -31
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
- package/package.json +7 -2
- package/src/AudioBrowser.ts +1 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +11 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +2 -2
- package/src/features/favorites.ts +13 -10
- package/src/features/hook-wiring.test.ts +63 -15
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/progress.ts +2 -1
- package/src/features/playback/rate.ts +8 -1
- package/src/features/playback/state.ts +2 -3
- package/src/features/playback/trackPlaybackTime.ts +1 -1
- package/src/features/player/options.ts +27 -19
- package/src/features/player/setup.test.ts +12 -3
- package/src/features/player/setup.ts +3 -1
- package/src/features/player/validateOptions.ts +2 -0
- package/src/features/queue/activeTrack.ts +14 -3
- package/src/features/queue/queue.ts +11 -4
- package/src/nitro-stub-drift.test.ts +40 -0
- package/src/public-surface.test.ts +40 -0
- package/src/specs/audio-browser.nitro.ts +4 -3
- package/src/style-resolution.test.ts +210 -0
- package/src/types/browser-native.ts +2 -2
- package/src/types/browser-nodes.ts +377 -135
- package/src/types/browser.ts +34 -55
- package/src/utils/LazyNativeEmitter.ts +20 -10
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.ts +37 -22
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/src/web/browser/BrowserManager.ts +176 -95
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- 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/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 +246 -0
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- 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/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- 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/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
package com.audiobrowser.browser
|
|
2
2
|
|
|
3
3
|
import android.util.LruCache
|
|
4
|
+
import androidx.media3.common.C
|
|
4
5
|
import androidx.media3.common.MediaItem
|
|
5
6
|
import androidx.media3.session.MediaSession
|
|
7
|
+
import com.audiobrowser.extension.identity
|
|
6
8
|
import com.audiobrowser.http.HttpClient
|
|
7
9
|
import com.audiobrowser.http.RequestConfigBuilder
|
|
8
10
|
import com.audiobrowser.util.BrowserPathHelper
|
|
@@ -10,7 +12,6 @@ import com.audiobrowser.util.TrackFactory
|
|
|
10
12
|
import com.audiobrowser.util.artworkOf
|
|
11
13
|
import com.margelo.nitro.audiobrowser.ArtworkRequestConfig
|
|
12
14
|
import com.margelo.nitro.audiobrowser.BrowserSourceCallbackParam
|
|
13
|
-
import com.margelo.nitro.audiobrowser.FavoritesMatchMode
|
|
14
15
|
import com.margelo.nitro.audiobrowser.Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____
|
|
15
16
|
import com.margelo.nitro.audiobrowser.ImageContext
|
|
16
17
|
import com.margelo.nitro.audiobrowser.MediaReference
|
|
@@ -19,6 +20,8 @@ import com.margelo.nitro.audiobrowser.NativeRouteEntry
|
|
|
19
20
|
import com.margelo.nitro.audiobrowser.RequestConfig
|
|
20
21
|
import com.margelo.nitro.audiobrowser.ResolvedTrack
|
|
21
22
|
import com.margelo.nitro.audiobrowser.SearchParams
|
|
23
|
+
import com.margelo.nitro.audiobrowser.Section
|
|
24
|
+
import com.margelo.nitro.audiobrowser.StyleDisplay
|
|
22
25
|
import com.margelo.nitro.audiobrowser.Track
|
|
23
26
|
import com.margelo.nitro.audiobrowser.TransformableRequestConfig
|
|
24
27
|
import kotlinx.coroutines.Dispatchers
|
|
@@ -82,7 +85,7 @@ class BrowserManager {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
// LRU cache for individual tracks - keyed by both
|
|
88
|
+
// LRU cache for individual tracks - keyed by both path and src for O(1) lookup
|
|
86
89
|
private val trackCache = LruCache<String, Track>(3000)
|
|
87
90
|
|
|
88
91
|
// LRU cache for resolved content - keyed by path
|
|
@@ -94,12 +97,12 @@ class BrowserManager {
|
|
|
94
97
|
// concurrent reader never sees one query matched with another query's results.
|
|
95
98
|
@Volatile private var lastSearch: Pair<String, Array<Track>>? = null
|
|
96
99
|
|
|
97
|
-
// Set of favorited track
|
|
100
|
+
// Set of favorited track identities (id when non-blank, else src — see Track.identity)
|
|
98
101
|
private var favoriteIds = setOf<String>()
|
|
99
102
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
private var
|
|
103
|
+
// Whether favoriting is enabled, propagated from the player's `favorite` capability.
|
|
104
|
+
// false = favoriting disabled (no row hearts). Set via setFavoriteEnabled.
|
|
105
|
+
private var favoriteEnabled: Boolean = false
|
|
103
106
|
|
|
104
107
|
// Navigation tracking to prevent race conditions
|
|
105
108
|
@Volatile private var currentNavigationId = 0
|
|
@@ -150,8 +153,8 @@ class BrowserManager {
|
|
|
150
153
|
val artworkResolutions = ArtworkResolutionRegistry()
|
|
151
154
|
|
|
152
155
|
/**
|
|
153
|
-
* Sets the favorited track
|
|
154
|
-
* this list during browsing.
|
|
156
|
+
* Sets the favorited track identities (id when non-blank, else src). Tracks will have their
|
|
157
|
+
* favorited field hydrated based on this list during browsing.
|
|
155
158
|
*/
|
|
156
159
|
fun setFavorites(favorites: List<String>) {
|
|
157
160
|
favoriteIds = favorites.toSet()
|
|
@@ -159,97 +162,125 @@ class BrowserManager {
|
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
/**
|
|
162
|
-
*
|
|
165
|
+
* Enables or disables favoriting (propagated from the `favorite` capability). false disables
|
|
163
166
|
* row-heart hydration.
|
|
164
167
|
*/
|
|
165
|
-
fun
|
|
166
|
-
|
|
168
|
+
fun setFavoriteEnabled(enabled: Boolean) {
|
|
169
|
+
favoriteEnabled = enabled
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
/**
|
|
170
|
-
* Updates the favorite state for a single track
|
|
171
|
-
* tapped in media controllers.
|
|
173
|
+
* Updates the favorite state for a single track identity (id when non-blank, else src). Called
|
|
174
|
+
* when the heart button is tapped in media controllers.
|
|
172
175
|
*/
|
|
173
|
-
fun updateFavorite(
|
|
176
|
+
fun updateFavorite(identity: String, favorited: Boolean) {
|
|
174
177
|
favoriteIds =
|
|
175
178
|
if (favorited) {
|
|
176
|
-
favoriteIds +
|
|
179
|
+
favoriteIds + identity
|
|
177
180
|
} else {
|
|
178
|
-
favoriteIds -
|
|
181
|
+
favoriteIds - identity
|
|
179
182
|
}
|
|
180
|
-
Timber.d("Updated favorite for '$
|
|
183
|
+
Timber.d("Updated favorite for '$identity' to $favorited (total: ${favoriteIds.size})")
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
/**
|
|
184
187
|
* Hydrates the favorited field on a track based on the favoriteIds set. No-op unless favoriting
|
|
185
|
-
* is enabled (the `favorite` capability). Only
|
|
186
|
-
* flag is set to true OR false so non-favorited tracks still show an (empty)
|
|
187
|
-
* overwrite API-provided values.
|
|
188
|
+
* is enabled (the `favorite` capability). Only tracks with an identity (id or src) are
|
|
189
|
+
* favoritable; the flag is set to true OR false so non-favorited tracks still show an (empty)
|
|
190
|
+
* heart. Doesn't overwrite API-provided values.
|
|
188
191
|
*/
|
|
189
192
|
private fun hydrateFavorite(track: Track): Track {
|
|
190
|
-
|
|
193
|
+
if (!favoriteEnabled) return track
|
|
191
194
|
// Don't overwrite API-provided favorites
|
|
192
195
|
if (track.favorited != null) return track
|
|
193
|
-
// Only
|
|
194
|
-
|
|
196
|
+
// Only identity-bearing tracks are favoritable
|
|
197
|
+
if (track.identity == null) return track
|
|
195
198
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return track.copy(favorited = isFavorited)
|
|
199
|
+
return track.copy(favorited = isFavorite(track))
|
|
199
200
|
}
|
|
200
201
|
|
|
201
|
-
/** Whether [
|
|
202
|
-
private fun isFavorite(
|
|
203
|
-
|
|
204
|
-
FavoritesMatchMode.EXACT -> favoriteIds.contains(src)
|
|
205
|
-
FavoritesMatchMode.PARTIAL -> favoriteIds.any { BrowserPathHelper.containsSegment(src, it) }
|
|
206
|
-
}
|
|
202
|
+
/** Whether [track]'s identity is in the favorites set. */
|
|
203
|
+
private fun isFavorite(track: Track): Boolean =
|
|
204
|
+
track.identity?.let { favoriteIds.contains(it) } == true
|
|
207
205
|
|
|
208
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Hydrates favorites on all tracks of a resolved page. Pages reach hydration normalized (ADR
|
|
208
|
+
* 0010), so `sections` is the only structure to walk.
|
|
209
|
+
*/
|
|
209
210
|
private fun hydrateChildren(resolvedTrack: ResolvedTrack): ResolvedTrack {
|
|
210
|
-
val
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
val sections = resolvedTrack.sections ?: return resolvedTrack
|
|
212
|
+
return resolvedTrack.copy(
|
|
213
|
+
sections =
|
|
214
|
+
sections
|
|
215
|
+
.map { section ->
|
|
216
|
+
section.copy(children = section.children.map { hydrateFavorite(it) }.toTypedArray())
|
|
217
|
+
}
|
|
218
|
+
.toTypedArray()
|
|
219
|
+
)
|
|
213
220
|
}
|
|
214
221
|
|
|
215
|
-
/** Cache a track by id,
|
|
222
|
+
/** Cache a track by id, path, and src for O(1) lookup from any mediaId form. */
|
|
216
223
|
private fun cacheTrack(track: Track) {
|
|
217
224
|
track.id?.takeUnless { it.isBlank() }?.let { trackCache.put(it, track) }
|
|
218
|
-
track.
|
|
225
|
+
track.path?.let { trackCache.put(it, track) }
|
|
219
226
|
track.src?.let { trackCache.put(it, track) }
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
private fun cacheChildren(resolvedTrack: ResolvedTrack) {
|
|
223
|
-
resolvedTrack.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
resolvedTrack.flattenedChildren?.forEach { cacheTrack(it) }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Pages already warned about by warnIfGridPageLacksPromise — the mismatch
|
|
234
|
+
// re-observes on every onGetChildren serve, and once is a diagnostic while
|
|
235
|
+
// every-scroll is noise. Concurrent: onGetChildren serves run on the IO
|
|
236
|
+
// pool, several in flight at once.
|
|
237
|
+
private val promiseWarnedPaths = java.util.concurrent.ConcurrentHashMap.newKeySet<String>()
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Dev diagnostic (ADR 0011): a page serving grid sections under a browsable handle that declared
|
|
241
|
+
* no `style.display` promise still renders its parent-level layout as a list on Android Auto —
|
|
242
|
+
* the parent hint is emitted only when declared, never derived from resolved pages and never
|
|
243
|
+
* back-filled. Warns at the one site where the mismatch is observable; a cache miss for the
|
|
244
|
+
* handle (cold start straight into the page) just skips the check.
|
|
245
|
+
*
|
|
246
|
+
* Only an all-grid page warns: the parent-level hint lays out the WHOLE page, so on a mixed page
|
|
247
|
+
* (a teaser shelf above list sections) there is no correct promise to advise — grid would tile
|
|
248
|
+
* the list sections too.
|
|
249
|
+
*/
|
|
250
|
+
fun warnIfGridPageLacksPromise(path: String, sections: List<Section>) {
|
|
251
|
+
if (sections.isEmpty() || !sections.all { it.style?.display == StyleDisplay.GRID }) return
|
|
252
|
+
val handle = trackCache.get(path) ?: return
|
|
253
|
+
if (handle.src != null) return // rendered playable — opens no page, promises nothing
|
|
254
|
+
if (handle.style?.display == null && promiseWarnedPaths.add(path)) {
|
|
255
|
+
Timber.w(
|
|
256
|
+
"Page '%s' serves grid sections, but its browsable handle declares no style.display — " +
|
|
257
|
+
"Android Auto's parent-level hint was not emitted, so this page renders as a list " +
|
|
258
|
+
"there. Declare `style: { display: 'grid' }` on the handle track.",
|
|
259
|
+
path,
|
|
260
|
+
)
|
|
231
261
|
}
|
|
232
262
|
}
|
|
233
263
|
|
|
234
264
|
/**
|
|
235
|
-
* Get a cached Track by mediaId (stable id,
|
|
265
|
+
* Get a cached Track by mediaId (stable id, path, or src), or null if not cached. Used by Media3
|
|
236
266
|
* to rehydrate MediaItem shells with full track metadata. Re-hydrates favorites in case
|
|
237
267
|
* setFavoriteStates was called after caching.
|
|
238
268
|
*/
|
|
239
269
|
fun getCachedTrack(mediaId: String): Track? {
|
|
240
|
-
// Try direct lookup first (matches
|
|
270
|
+
// Try direct lookup first (matches path or src)
|
|
241
271
|
trackCache.get(mediaId)?.let { track ->
|
|
242
272
|
val hydratedTrack = hydrateFavorite(track)
|
|
243
273
|
Timber.d("Cache HIT for mediaId='$mediaId' → '${track.title}'")
|
|
244
274
|
return hydratedTrack
|
|
245
275
|
}
|
|
246
276
|
|
|
247
|
-
// Try extracting src from contextual
|
|
277
|
+
// Try extracting the track identity (id when non-blank, else src) from a contextual path —
|
|
278
|
+
// the cache is keyed by id AND src, so either identity form resolves.
|
|
248
279
|
val trackId = BrowserPathHelper.extractTrackId(mediaId)
|
|
249
280
|
if (trackId != null) {
|
|
250
281
|
trackCache.get(trackId)?.let { track ->
|
|
251
282
|
val hydratedTrack = hydrateFavorite(track)
|
|
252
|
-
Timber.d("Cache HIT (extracted
|
|
283
|
+
Timber.d("Cache HIT (extracted identity) for mediaId='$mediaId' → '${track.title}'")
|
|
253
284
|
return hydratedTrack
|
|
254
285
|
}
|
|
255
286
|
}
|
|
@@ -259,19 +290,19 @@ class BrowserManager {
|
|
|
259
290
|
}
|
|
260
291
|
|
|
261
292
|
/**
|
|
262
|
-
* Resolves a tapped mediaId to the contextual
|
|
293
|
+
* Resolves a tapped mediaId to the contextual path to expand a queue from, or null when there is
|
|
263
294
|
* none. A contextual mediaId is its own; a stable-id mediaId (see TrackFactory.buildMediaItem)
|
|
264
|
-
* resolves through the track cache to the contextual
|
|
295
|
+
* resolves through the track cache to the contextual path of the container it was most recently
|
|
265
296
|
* browsed in — a legacy car controller round-trips only the mediaId, so the cache is what
|
|
266
297
|
* remembers which list the row came from.
|
|
267
298
|
*/
|
|
268
|
-
fun
|
|
299
|
+
fun contextualPathFor(mediaId: String): String? {
|
|
269
300
|
if (BrowserPathHelper.isContextual(mediaId)) return mediaId
|
|
270
|
-
return getCachedTrack(mediaId)?.
|
|
301
|
+
return getCachedTrack(mediaId)?.path?.takeIf { BrowserPathHelper.isContextual(it) }
|
|
271
302
|
}
|
|
272
303
|
|
|
273
304
|
/**
|
|
274
|
-
* Resolves a single Media3 MediaItem to a Track. Prefers the track cache (keyed by id,
|
|
305
|
+
* Resolves a single Media3 MediaItem to a Track. Prefers the track cache (keyed by id, path, and
|
|
275
306
|
* src).
|
|
276
307
|
*
|
|
277
308
|
* A cache miss is legitimately reachable: a controller can replay a mediaId this process never
|
|
@@ -304,26 +335,23 @@ class BrowserManager {
|
|
|
304
335
|
// A mediaId distinct from the playable uri is the track's stable id —
|
|
305
336
|
// keep it so the item's identity (car now-playing row match) survives.
|
|
306
337
|
id = mediaId.takeIf { it != fallbackSrc },
|
|
307
|
-
|
|
338
|
+
path = null,
|
|
308
339
|
src = fallbackSrc,
|
|
309
340
|
artwork = artworkOf(metadata.artworkUri?.toString()),
|
|
310
341
|
artworkSource = null,
|
|
311
342
|
request = null,
|
|
312
|
-
artworkCarPlayTinted = null,
|
|
313
343
|
title = metadata.title?.toString() ?: mediaId,
|
|
314
344
|
subtitle = metadata.artist?.toString(),
|
|
315
345
|
artist = null,
|
|
316
|
-
|
|
346
|
+
albumPath = null,
|
|
317
347
|
album = metadata.albumTitle?.toString(),
|
|
318
348
|
description = metadata.description?.toString(),
|
|
319
349
|
genre = metadata.genre?.toString(),
|
|
320
350
|
duration = null,
|
|
321
351
|
style = null,
|
|
322
|
-
|
|
352
|
+
disabled = null,
|
|
323
353
|
favorited = null,
|
|
324
|
-
groupTitle = null,
|
|
325
354
|
live = null,
|
|
326
|
-
imageRow = null,
|
|
327
355
|
)
|
|
328
356
|
)
|
|
329
357
|
}
|
|
@@ -361,17 +389,19 @@ class BrowserManager {
|
|
|
361
389
|
if (searchQuery != null) {
|
|
362
390
|
Timber.d("Handling search playback request for query: $searchQuery")
|
|
363
391
|
|
|
364
|
-
// Execute search (will hit cache if already performed)
|
|
392
|
+
// Execute search (will hit cache if already performed). Disabled
|
|
393
|
+
// tracks never play, so they never enter a search-built queue.
|
|
365
394
|
val searchResults = search(searchQuery)
|
|
366
|
-
val searchTracks =
|
|
395
|
+
val searchTracks =
|
|
396
|
+
searchResults.flattenedChildren?.filterNot { it.disabled == true }?.toTypedArray()
|
|
367
397
|
|
|
368
398
|
if (searchTracks != null && searchTracks.isNotEmpty()) {
|
|
369
399
|
// Find the selected track in search results (mediaId is the stable id
|
|
370
|
-
// when the track has one, else
|
|
400
|
+
// when the track has one, else path/src — see TrackFactory)
|
|
371
401
|
val mediaId = mediaItem.mediaId
|
|
372
402
|
val selectedIndex =
|
|
373
403
|
searchTracks.indexOfFirst { track ->
|
|
374
|
-
track.id == mediaId || track.
|
|
404
|
+
track.id == mediaId || track.path == mediaId || track.src == mediaId
|
|
375
405
|
}
|
|
376
406
|
|
|
377
407
|
if (selectedIndex >= 0) {
|
|
@@ -398,12 +428,12 @@ class BrowserManager {
|
|
|
398
428
|
val mediaId = mediaItems[0].mediaId
|
|
399
429
|
// A failed search match falls through to plain resolution, never to browse
|
|
400
430
|
// expansion — the user asked for that one result, not a browsed list.
|
|
401
|
-
val
|
|
431
|
+
val contextualPath = if (searchQuery == null) contextualPathFor(mediaId) else null
|
|
402
432
|
|
|
403
|
-
if (
|
|
404
|
-
Timber.d("Attempting queue expansion for mediaId='$mediaId' via '$
|
|
433
|
+
if (contextualPath != null) {
|
|
434
|
+
Timber.d("Attempting queue expansion for mediaId='$mediaId' via '$contextualPath'")
|
|
405
435
|
|
|
406
|
-
val expanded =
|
|
436
|
+
val expanded = expandQueueFromContextualPath(contextualPath)
|
|
407
437
|
|
|
408
438
|
if (expanded != null) {
|
|
409
439
|
val (tracks, selectedIndex) = expanded
|
|
@@ -423,20 +453,56 @@ class BrowserManager {
|
|
|
423
453
|
// No expansion - resolve from cache (with a minimal-track fallback for replayed mediaIds)
|
|
424
454
|
val resolvedTracks = mediaItems.map { resolveMediaItemToTrack(it) }
|
|
425
455
|
|
|
456
|
+
// A disabled track is unavailable — it never plays, even by a stale or
|
|
457
|
+
// replayed mediaId (Track.disabled). Refusal fails the future: Media3
|
|
458
|
+
// surfaces an error to the controller and the current playback stays
|
|
459
|
+
// untouched (an empty item list would clear the live timeline instead).
|
|
460
|
+
//
|
|
461
|
+
// Media3's legacy path — how Android Auto and Assistant actually request
|
|
462
|
+
// playback (onPlayFromMediaId et al.) — passes C.INDEX_UNSET, meaning
|
|
463
|
+
// "the default item", i.e. the first. Normalize before the guard, or the
|
|
464
|
+
// refusal is dead on exactly the path that serves cars.
|
|
465
|
+
val effectiveStartIndex = if (startIndex == C.INDEX_UNSET) 0 else startIndex
|
|
466
|
+
val startTrack = resolvedTracks.getOrNull(effectiveStartIndex)
|
|
467
|
+
if (startTrack != null && startTrack.disabled == true) {
|
|
468
|
+
Timber.w("Refusing playback of disabled track: ${startTrack.title}")
|
|
469
|
+
throw IllegalStateException("Refusing playback of disabled track: ${startTrack.title}")
|
|
470
|
+
}
|
|
471
|
+
val playableTracks = resolvedTracks.filterNot { it.disabled == true }
|
|
472
|
+
if (playableTracks.isEmpty()) {
|
|
473
|
+
// Belt over the start-item guard: never hand Media3 an empty timeline.
|
|
474
|
+
Timber.w("Refusing playback: every requested track is disabled")
|
|
475
|
+
throw IllegalStateException("Refusing playback: every requested track is disabled")
|
|
476
|
+
}
|
|
477
|
+
// Positional, not indexOf: value equality would alias a duplicated track
|
|
478
|
+
// (the same station twice in a queue) to its first copy. An unset index
|
|
479
|
+
// stays unset — Media3's "default item" semantics pass through as they
|
|
480
|
+
// did before the filter existed.
|
|
481
|
+
val adjustedStartIndex =
|
|
482
|
+
when {
|
|
483
|
+
startIndex == C.INDEX_UNSET -> C.INDEX_UNSET
|
|
484
|
+
startTrack == null -> 0
|
|
485
|
+
else -> resolvedTracks.take(effectiveStartIndex).count { it.disabled != true }
|
|
486
|
+
}
|
|
487
|
+
|
|
426
488
|
// Convert to Media3 MediaItems
|
|
427
|
-
val resolvedMediaItems =
|
|
489
|
+
val resolvedMediaItems = playableTracks.map { track -> TrackFactory.toMedia3(track) }
|
|
428
490
|
|
|
429
|
-
return MediaSession.MediaItemsWithStartPosition(
|
|
491
|
+
return MediaSession.MediaItemsWithStartPosition(
|
|
492
|
+
resolvedMediaItems,
|
|
493
|
+
adjustedStartIndex,
|
|
494
|
+
startPositionMs,
|
|
495
|
+
)
|
|
430
496
|
}
|
|
431
497
|
|
|
432
498
|
/**
|
|
433
|
-
* Validates that a track has either
|
|
499
|
+
* Validates that a track has either path or src for stable identification. Throws
|
|
434
500
|
* IllegalStateException if validation fails.
|
|
435
501
|
*/
|
|
436
502
|
private fun validateTrack(track: Track, context: String) {
|
|
437
|
-
if (track.
|
|
503
|
+
if (track.path == null && track.src == null) {
|
|
438
504
|
throw IllegalStateException(
|
|
439
|
-
"$context must have either '
|
|
505
|
+
"$context must have either 'path' or 'src' property for stable identification. Track: ${track.title}"
|
|
440
506
|
)
|
|
441
507
|
}
|
|
442
508
|
}
|
|
@@ -444,12 +510,12 @@ class BrowserManager {
|
|
|
444
510
|
suspend fun resolve(path: String, useCache: Boolean = true): ResolvedTrack {
|
|
445
511
|
Timber.d("=== RESOLVE: path='$path' (useCache=$useCache) ===")
|
|
446
512
|
|
|
447
|
-
// Strip __trackId from contextual
|
|
513
|
+
// Strip __trackId from contextual paths (e.g., "/library/radio?__trackId=song.mp3" →
|
|
448
514
|
// "/library/radio")
|
|
449
|
-
// This allows resolving the parent container for tracks referenced by contextual
|
|
515
|
+
// This allows resolving the parent container for tracks referenced by contextual path
|
|
450
516
|
val normalizedPath = BrowserPathHelper.stripTrackId(path)
|
|
451
517
|
if (normalizedPath != path) {
|
|
452
|
-
Timber.d("Stripped __trackId from contextual
|
|
518
|
+
Timber.d("Stripped __trackId from contextual path: '$normalizedPath'")
|
|
453
519
|
}
|
|
454
520
|
|
|
455
521
|
// Check content cache first
|
|
@@ -457,7 +523,7 @@ class BrowserManager {
|
|
|
457
523
|
contentCache.get(normalizedPath)?.let { cached ->
|
|
458
524
|
Timber.d("Content cache HIT for path='$normalizedPath'")
|
|
459
525
|
// Re-key the track cache even on a hit: an id-keyed lookup (stable-id
|
|
460
|
-
// mediaId → contextual
|
|
526
|
+
// mediaId → contextual path, see contextualPathFor) must reflect the
|
|
461
527
|
// most recently *browsed* container, which a cached re-display
|
|
462
528
|
// otherwise wouldn't re-register.
|
|
463
529
|
cacheChildren(cached)
|
|
@@ -483,11 +549,11 @@ class BrowserManager {
|
|
|
483
549
|
* (e.g., via notifyContentChanged).
|
|
484
550
|
*
|
|
485
551
|
* @param path The container path to invalidate (e.g., "/library/radio")
|
|
486
|
-
* @throws IllegalArgumentException if passed a contextual
|
|
552
|
+
* @throws IllegalArgumentException if passed a contextual path (contains __trackId)
|
|
487
553
|
*/
|
|
488
554
|
fun invalidateContentCache(path: String) {
|
|
489
555
|
require(!BrowserPathHelper.isContextual(path)) {
|
|
490
|
-
"invalidateContentCache() expects a container path, not a contextual
|
|
556
|
+
"invalidateContentCache() expects a container path, not a contextual path: $path"
|
|
491
557
|
}
|
|
492
558
|
contentCache.remove(path)
|
|
493
559
|
Timber.d("Invalidated content cache for path='$path'")
|
|
@@ -496,6 +562,10 @@ class BrowserManager {
|
|
|
496
562
|
/** Clears all cached content. */
|
|
497
563
|
fun clearContentCache() {
|
|
498
564
|
contentCache.evictAll()
|
|
565
|
+
// The search slot is content too: a locale switch or a re-auth changes titles, src, artwork
|
|
566
|
+
// and availability, and nothing else ever evicts it, so a repeated query returned
|
|
567
|
+
// pre-invalidation results forever. (Matches the Swift twin's clearContentCache.)
|
|
568
|
+
lastSearch = null
|
|
499
569
|
// Bump the layer generation so request/browse resolver thunks are re-resolved on the next
|
|
500
570
|
// request (invalidateAllContent → clearContentCache is the documented re-resolve trigger).
|
|
501
571
|
layerGeneration += 1
|
|
@@ -524,81 +594,71 @@ class BrowserManager {
|
|
|
524
594
|
effectiveArtworkConfig = config.artwork
|
|
525
595
|
}
|
|
526
596
|
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
597
|
+
// Normalize to the canonical sectioned shape (children sugar → one untitled
|
|
598
|
+
// section — ADR 0010), then generate contextual paths and transform artwork
|
|
599
|
+
// URLs. The output never carries `children`.
|
|
600
|
+
val sections = resolvedTrack.normalizedSections ?: return resolvedTrack
|
|
601
|
+
// Dev diagnostic (ADR 0011) — before the blocks are folded, while each level is
|
|
602
|
+
// still attributable. Every page passes here whatever its source, and only on a
|
|
603
|
+
// cache miss. No-op outside debug builds.
|
|
604
|
+
InertStyleDiagnostic.warn(path, resolvedTrack.style, sections)
|
|
605
|
+
// The contextual index is the flat page position — children concatenated in
|
|
606
|
+
// section order (ADR 0009/0010).
|
|
607
|
+
var flatIndex = 0
|
|
608
|
+
// One scope for the whole page: every child's transform (which can suspend
|
|
609
|
+
// on artwork resolution) launches up front across all sections, then the
|
|
610
|
+
// sections reassemble — a slow section doesn't serialize the ones after it.
|
|
611
|
+
val transformedSections = coroutineScope {
|
|
612
|
+
sections
|
|
613
|
+
.map { section ->
|
|
614
|
+
val base = flatIndex
|
|
615
|
+
flatIndex += section.children.size
|
|
616
|
+
section to
|
|
617
|
+
section.children.mapIndexed { offset, track ->
|
|
533
618
|
async {
|
|
534
|
-
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
// identity; on Android Auto it expands into them).
|
|
538
|
-
if (track.imageRow.isNullOrEmpty()) validateTrack(track, "Child track")
|
|
619
|
+
val index = base + offset
|
|
620
|
+
// Validate that track has stable identifier.
|
|
621
|
+
validateTrack(track, "Child track")
|
|
539
622
|
|
|
540
623
|
var transformedTrack = track
|
|
541
624
|
|
|
542
|
-
// Generate contextual
|
|
625
|
+
// Generate contextual paths for playable tracks
|
|
543
626
|
// Always regenerate to reflect the current browsing context, not the original
|
|
544
627
|
// context
|
|
545
628
|
// (e.g., a track favorited from an album should use /favorites context when browsed
|
|
546
629
|
// there)
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
630
|
+
// The flat page position rides along as a duplicate-identity tie-breaker.
|
|
631
|
+
val trackIdentity = track.identity
|
|
632
|
+
if (track.src != null && trackIdentity != null) {
|
|
633
|
+
val contextualPath = BrowserPathHelper.build(path, trackIdentity, index)
|
|
634
|
+
transformedTrack = transformedTrack.copy(path = contextualPath)
|
|
550
635
|
|
|
551
636
|
Timber.d(
|
|
552
|
-
"[$path] Child[$index] '${track.title}': Playable,
|
|
637
|
+
"[$path] Child[$index] '${track.title}': Playable, contextualPath=$contextualPath (identity=$trackIdentity)"
|
|
553
638
|
)
|
|
554
639
|
} else {
|
|
555
640
|
Timber.d(
|
|
556
|
-
"[$path] Child[$index] '${track.title}': Browsable with
|
|
641
|
+
"[$path] Child[$index] '${track.title}': Browsable with path=${track.path}"
|
|
557
642
|
)
|
|
558
643
|
}
|
|
559
644
|
|
|
560
|
-
// Playable image-row items get contextual URLs like any list
|
|
561
|
-
// row, so a tile tap expands into its section's queue instead
|
|
562
|
-
// of a queue of one (ADR 0006).
|
|
563
|
-
transformedTrack.imageRow?.let { items ->
|
|
564
|
-
transformedTrack =
|
|
565
|
-
transformedTrack.copy(
|
|
566
|
-
imageRow =
|
|
567
|
-
items
|
|
568
|
-
.map { item ->
|
|
569
|
-
if (item.url == null && item.src != null) {
|
|
570
|
-
item.copy(url = BrowserPathHelper.build(path, item.src))
|
|
571
|
-
} else {
|
|
572
|
-
item
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
.toTypedArray()
|
|
576
|
-
)
|
|
577
|
-
}
|
|
578
|
-
|
|
579
645
|
// Transform artwork URL. At browse-time there is no display size info.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
)
|
|
588
|
-
|
|
589
|
-
transformedTrack
|
|
646
|
+
transformArtworkUrl(
|
|
647
|
+
transformedTrack,
|
|
648
|
+
effectiveArtworkConfig,
|
|
649
|
+
path,
|
|
650
|
+
index,
|
|
651
|
+
ImageContext(null, null),
|
|
652
|
+
)
|
|
590
653
|
}
|
|
591
654
|
}
|
|
592
|
-
.awaitAll()
|
|
593
655
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
return if (transformedChildren != null) {
|
|
598
|
-
resolvedTrack.copy(children = transformedChildren.toTypedArray())
|
|
599
|
-
} else {
|
|
600
|
-
resolvedTrack
|
|
656
|
+
.map { (section, transformedChildren) ->
|
|
657
|
+
section.copy(children = transformedChildren.awaitAll().toTypedArray())
|
|
658
|
+
}
|
|
601
659
|
}
|
|
660
|
+
|
|
661
|
+
return resolvedTrack.copy(sections = transformedSections.toTypedArray(), children = null)
|
|
602
662
|
}
|
|
603
663
|
|
|
604
664
|
/**
|
|
@@ -657,55 +717,68 @@ class BrowserManager {
|
|
|
657
717
|
}
|
|
658
718
|
|
|
659
719
|
/**
|
|
660
|
-
* Expands a contextual
|
|
720
|
+
* Expands a contextual path into a queue of playable tracks.
|
|
661
721
|
*
|
|
662
722
|
* Used when navigating to a track to load it with its full album/playlist context. Returns only
|
|
663
723
|
* the selected track if singleTrack is true.
|
|
664
724
|
*
|
|
665
|
-
* @param
|
|
725
|
+
* @param contextualPath The contextual path (e.g., "/album?__trackId=song.mp3")
|
|
666
726
|
* @return Pair of (tracks array, selected track index), or null if expansion fails
|
|
667
727
|
*/
|
|
668
|
-
suspend fun
|
|
669
|
-
val trackId = BrowserPathHelper.extractTrackId(
|
|
728
|
+
suspend fun expandQueueFromContextualPath(contextualPath: String): Pair<Array<Track>, Int>? {
|
|
729
|
+
val trackId = BrowserPathHelper.extractTrackId(contextualPath) ?: return null
|
|
670
730
|
|
|
671
|
-
Timber.d("Expanding queue from contextual
|
|
731
|
+
Timber.d("Expanding queue from contextual path: $contextualPath (trackId=$trackId)")
|
|
672
732
|
|
|
673
733
|
try {
|
|
674
734
|
// Resolve the parent container to get all siblings
|
|
675
|
-
val parentPath = BrowserPathHelper.stripTrackId(
|
|
735
|
+
val parentPath = BrowserPathHelper.stripTrackId(contextualPath)
|
|
676
736
|
val parentResolvedTrack = resolve(parentPath)
|
|
677
|
-
val
|
|
737
|
+
val sections = parentResolvedTrack.normalizedSections
|
|
678
738
|
|
|
679
|
-
if (
|
|
739
|
+
if (sections.isNullOrEmpty()) {
|
|
680
740
|
Timber.w("Parent has no children, cannot expand queue")
|
|
681
741
|
return null
|
|
682
742
|
}
|
|
683
743
|
|
|
684
|
-
// Queue scope is the tapped section, not the whole page (ADR 0006).
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
// Filter to only playable tracks (tracks with src)
|
|
697
|
-
|
|
744
|
+
// Queue scope is the tapped section, not the whole page (ADR 0006). The
|
|
745
|
+
// stamped flat index pins which section (and which copy) was tapped when
|
|
746
|
+
// the same identity appears more than once; it is only a tie-breaker — a
|
|
747
|
+
// stale index falls back to the first identity match. An id that no
|
|
748
|
+
// longer appears on the page at all aborts the expansion — the caller
|
|
749
|
+
// falls back to the stored single track; silently queueing the changed
|
|
750
|
+
// list would resume the wrong station.
|
|
751
|
+
val tappedIndex = BrowserPathHelper.extractIndex(contextualPath)
|
|
752
|
+
val scoped = SectionScope.scoped(sections, trackId, tappedIndex) ?: return null
|
|
753
|
+
val sectionTracks = scoped.section.children.toList()
|
|
754
|
+
val tappedOffset = scoped.tappedOffset
|
|
755
|
+
|
|
756
|
+
// Filter to only playable tracks (tracks with src). A disabled track is
|
|
757
|
+
// unavailable — queue expansion excludes it, so auto-advance never meets
|
|
758
|
+
// one.
|
|
759
|
+
val playableTracks =
|
|
760
|
+
sectionTracks.filter { track -> track.src != null && track.disabled != true }
|
|
698
761
|
|
|
699
762
|
if (playableTracks.isEmpty()) {
|
|
700
763
|
Timber.w("Parent has no playable tracks, cannot expand queue")
|
|
701
764
|
return null
|
|
702
765
|
}
|
|
703
766
|
|
|
704
|
-
// Find the index of the selected track in the playable tracks array
|
|
705
|
-
|
|
767
|
+
// Find the index of the selected track in the playable tracks array:
|
|
768
|
+
// the pinned copy when the stamp survived, else the first identity match
|
|
769
|
+
val selectedIndex =
|
|
770
|
+
if (
|
|
771
|
+
tappedOffset != null &&
|
|
772
|
+
sectionTracks[tappedOffset].src != null &&
|
|
773
|
+
sectionTracks[tappedOffset].disabled != true
|
|
774
|
+
) {
|
|
775
|
+
sectionTracks.take(tappedOffset + 1).count { it.src != null && it.disabled != true } - 1
|
|
776
|
+
} else {
|
|
777
|
+
playableTracks.indexOfFirst { track -> track.identity == trackId }
|
|
778
|
+
}
|
|
706
779
|
|
|
707
780
|
if (selectedIndex < 0) {
|
|
708
|
-
Timber.w("Track with
|
|
781
|
+
Timber.w("Track with identity='$trackId' not found in playable children")
|
|
709
782
|
return null
|
|
710
783
|
}
|
|
711
784
|
|
|
@@ -716,11 +789,11 @@ class BrowserManager {
|
|
|
716
789
|
}
|
|
717
790
|
|
|
718
791
|
Timber.d(
|
|
719
|
-
"singleTrack=false - returning ${playableTracks.size} playable tracks (from ${
|
|
792
|
+
"singleTrack=false - returning ${playableTracks.size} playable tracks (from ${sectionTracks.size} in section), starting at index $selectedIndex"
|
|
720
793
|
)
|
|
721
794
|
return Pair(playableTracks.toTypedArray(), selectedIndex)
|
|
722
795
|
} catch (e: Exception) {
|
|
723
|
-
Timber.e(e, "Error expanding queue from contextual
|
|
796
|
+
Timber.e(e, "Error expanding queue from contextual path: $contextualPath")
|
|
724
797
|
return null
|
|
725
798
|
}
|
|
726
799
|
}
|
|
@@ -822,7 +895,10 @@ class BrowserManager {
|
|
|
822
895
|
*/
|
|
823
896
|
suspend fun searchPlayable(params: SearchParams): Array<Track>? {
|
|
824
897
|
val searchResults = search(params)
|
|
825
|
-
|
|
898
|
+
// Voice search never matches a disabled track (Track.disabled) — filtered
|
|
899
|
+
// before the first result is picked, so an unavailable one can't capture
|
|
900
|
+
// the play.
|
|
901
|
+
val tracks = searchResults.flattenedChildren?.filter { it.disabled != true }?.toTypedArray()
|
|
826
902
|
|
|
827
903
|
if (tracks.isNullOrEmpty()) {
|
|
828
904
|
return null
|
|
@@ -832,13 +908,13 @@ class BrowserManager {
|
|
|
832
908
|
|
|
833
909
|
// Check if result is browsable-only (container/route) vs playable
|
|
834
910
|
// If it's browsable but also playable (has src or playable=true), treat it as playable
|
|
835
|
-
val
|
|
911
|
+
val firstResultPath = firstResult.path
|
|
836
912
|
val tracksToFilter =
|
|
837
|
-
if (firstResult.src == null &&
|
|
838
|
-
Timber.d("First search result is browsable-only, resolving: $
|
|
839
|
-
val resolvedTrack = resolve(
|
|
840
|
-
resolvedTrack.
|
|
841
|
-
?.filter { it.src != null }
|
|
913
|
+
if (firstResult.src == null && firstResultPath != null) {
|
|
914
|
+
Timber.d("First search result is browsable-only, resolving: $firstResultPath")
|
|
915
|
+
val resolvedTrack = resolve(firstResultPath)
|
|
916
|
+
resolvedTrack.flattenedChildren
|
|
917
|
+
?.filter { it.src != null && it.disabled != true }
|
|
842
918
|
?.takeIf { it.isNotEmpty() }
|
|
843
919
|
?.toTypedArray() ?: tracks
|
|
844
920
|
} else {
|
|
@@ -883,34 +959,35 @@ class BrowserManager {
|
|
|
883
959
|
val searchPath = BrowserPathHelper.createSearchPath(params.query)
|
|
884
960
|
|
|
885
961
|
try {
|
|
886
|
-
// Execute search. Drop results without a stable identifier (
|
|
962
|
+
// Execute search. Drop results without a stable identifier (path or src): search results come
|
|
887
963
|
// from server/JS data that doesn't pass through validateTrack like browse children do, and
|
|
888
964
|
// downstream conversion (TrackFactory.toMedia3) and browsable-result resolution require one.
|
|
889
965
|
val searchResults =
|
|
890
966
|
resolveSearch(params)
|
|
891
967
|
.filter { track ->
|
|
892
|
-
val valid = track.
|
|
968
|
+
val valid = track.path != null || track.src != null
|
|
893
969
|
if (!valid) {
|
|
894
|
-
Timber.w("Dropping search result without
|
|
970
|
+
Timber.w("Dropping search result without path or src: '${track.title}'")
|
|
895
971
|
}
|
|
896
972
|
valid
|
|
897
973
|
}
|
|
898
974
|
.toTypedArray()
|
|
899
975
|
|
|
900
|
-
//
|
|
976
|
+
// Search results are a flat list; as a resolved page they are one
|
|
977
|
+
// untitled section (ADR 0010).
|
|
901
978
|
val searchResolvedTrack =
|
|
902
979
|
ResolvedTrack(
|
|
903
980
|
id = null,
|
|
904
|
-
|
|
981
|
+
path = searchPath,
|
|
905
982
|
title = "Search: ${params.query}",
|
|
906
|
-
|
|
983
|
+
sections = arrayOf(untitledSection(searchResults)),
|
|
984
|
+
children = null,
|
|
907
985
|
carPlaySiriListButton = null,
|
|
908
986
|
artwork = null,
|
|
909
987
|
artworkSource = null,
|
|
910
988
|
request = null,
|
|
911
|
-
artworkCarPlayTinted = null,
|
|
912
989
|
artist = null,
|
|
913
|
-
|
|
990
|
+
albumPath = null,
|
|
914
991
|
description = null,
|
|
915
992
|
subtitle = null,
|
|
916
993
|
album = null,
|
|
@@ -918,11 +995,9 @@ class BrowserManager {
|
|
|
918
995
|
duration = null,
|
|
919
996
|
src = null,
|
|
920
997
|
style = null,
|
|
921
|
-
|
|
998
|
+
disabled = null,
|
|
922
999
|
favorited = null,
|
|
923
|
-
groupTitle = null,
|
|
924
1000
|
live = null,
|
|
925
|
-
imageRow = null,
|
|
926
1001
|
)
|
|
927
1002
|
|
|
928
1003
|
// Cache search results for getCachedSearchResults()
|
|
@@ -942,16 +1017,16 @@ class BrowserManager {
|
|
|
942
1017
|
val emptySearchResult =
|
|
943
1018
|
ResolvedTrack(
|
|
944
1019
|
id = null,
|
|
945
|
-
|
|
1020
|
+
path = searchPath,
|
|
946
1021
|
title = "Search: ${params.query}",
|
|
947
|
-
|
|
1022
|
+
sections = arrayOf(untitledSection(emptyArray())),
|
|
1023
|
+
children = null,
|
|
948
1024
|
carPlaySiriListButton = null,
|
|
949
1025
|
artwork = null,
|
|
950
1026
|
artworkSource = null,
|
|
951
1027
|
request = null,
|
|
952
|
-
artworkCarPlayTinted = null,
|
|
953
1028
|
artist = null,
|
|
954
|
-
|
|
1029
|
+
albumPath = null,
|
|
955
1030
|
description = null,
|
|
956
1031
|
subtitle = null,
|
|
957
1032
|
album = null,
|
|
@@ -959,11 +1034,9 @@ class BrowserManager {
|
|
|
959
1034
|
duration = null,
|
|
960
1035
|
src = null,
|
|
961
1036
|
style = null,
|
|
962
|
-
|
|
1037
|
+
disabled = null,
|
|
963
1038
|
favorited = null,
|
|
964
|
-
groupTitle = null,
|
|
965
1039
|
live = null,
|
|
966
|
-
imageRow = null,
|
|
967
1040
|
)
|
|
968
1041
|
|
|
969
1042
|
return emptySearchResult
|
|
@@ -1040,7 +1113,7 @@ class BrowserManager {
|
|
|
1040
1113
|
// Validate tabs have stable identifiers
|
|
1041
1114
|
tabs.forEachIndexed { index, tab ->
|
|
1042
1115
|
validateTrack(tab, "Tab")
|
|
1043
|
-
Timber.d("[TABS] Tab[$index] '${tab.title}':
|
|
1116
|
+
Timber.d("[TABS] Tab[$index] '${tab.title}': path=${tab.path}")
|
|
1044
1117
|
}
|
|
1045
1118
|
|
|
1046
1119
|
this.tabs = tabs
|
|
@@ -1178,7 +1251,8 @@ class BrowserManager {
|
|
|
1178
1251
|
Timber.d("Resolving tabs via route entry")
|
|
1179
1252
|
val resolvedTrack = resolveRouteEntry(tabsEntry, TABS_ROUTE_PATH, emptyMap())
|
|
1180
1253
|
|
|
1181
|
-
|
|
1254
|
+
// Tabs are a flat list, not a page — a sectioned tabs source flattens.
|
|
1255
|
+
return resolvedTrack.flattenedChildren?.toTypedArray() ?: emptyArray()
|
|
1182
1256
|
}
|
|
1183
1257
|
|
|
1184
1258
|
/**
|