react-native-audio-browser 0.2.0-next.11 → 0.2.0-next.12
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 +4 -4
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -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/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 +93 -73
- package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- 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 +35 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +26 -39
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +228 -50
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -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/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 +131 -143
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/JsonModels.swift +187 -106
- package/ios/Browser/SearchDrillIn.swift +6 -6
- package/ios/Browser/SectionPresentation.swift +34 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +47 -0
- package/ios/Browser/TabBarEntries.swift +2 -2
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +105 -67
- package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
- package/ios/CarPlay/CarPlayListItemFactory.swift +217 -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/HybridAudioBrowser.swift +113 -35
- package/ios/Model/NitroTypeStubs.swift +46 -21
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Player/NowPlayingUpdater.swift +1 -1
- package/ios/Player/PlaybackCoordinator.swift +2 -2
- package/ios/Player/RetryManager.swift +4 -3
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/JsonModelsTests.swift +75 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +61 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +67 -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/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- 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/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/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/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/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/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 +260 -127
- 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 +4 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -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 +10 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -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 +4 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
- package/nitrogen/generated/android/c++/JSection.hpp +114 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -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/IOSOptions.kt +6 -3
- 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/Section.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
- 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 +23 -5
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- 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 +64 -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 +28 -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++/IOSOptions.hpp +9 -2
- 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 +96 -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 +50 -34
- package/package.json +1 -1
- 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/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 +139 -0
- package/src/types/browser-native.ts +2 -2
- package/src/types/browser-nodes.ts +268 -136
- 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/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/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'")
|
|
@@ -524,81 +590,67 @@ class BrowserManager {
|
|
|
524
590
|
effectiveArtworkConfig = config.artwork
|
|
525
591
|
}
|
|
526
592
|
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
593
|
+
// Normalize to the canonical sectioned shape (children sugar → one untitled
|
|
594
|
+
// section — ADR 0010), then generate contextual paths and transform artwork
|
|
595
|
+
// URLs. The output never carries `children`.
|
|
596
|
+
val sections = resolvedTrack.normalizedSections ?: return resolvedTrack
|
|
597
|
+
// The contextual index is the flat page position — children concatenated in
|
|
598
|
+
// section order (ADR 0009/0010).
|
|
599
|
+
var flatIndex = 0
|
|
600
|
+
// One scope for the whole page: every child's transform (which can suspend
|
|
601
|
+
// on artwork resolution) launches up front across all sections, then the
|
|
602
|
+
// sections reassemble — a slow section doesn't serialize the ones after it.
|
|
603
|
+
val transformedSections = coroutineScope {
|
|
604
|
+
sections
|
|
605
|
+
.map { section ->
|
|
606
|
+
val base = flatIndex
|
|
607
|
+
flatIndex += section.children.size
|
|
608
|
+
section to
|
|
609
|
+
section.children.mapIndexed { offset, track ->
|
|
533
610
|
async {
|
|
534
|
-
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
// identity; on Android Auto it expands into them).
|
|
538
|
-
if (track.imageRow.isNullOrEmpty()) validateTrack(track, "Child track")
|
|
611
|
+
val index = base + offset
|
|
612
|
+
// Validate that track has stable identifier.
|
|
613
|
+
validateTrack(track, "Child track")
|
|
539
614
|
|
|
540
615
|
var transformedTrack = track
|
|
541
616
|
|
|
542
|
-
// Generate contextual
|
|
617
|
+
// Generate contextual paths for playable tracks
|
|
543
618
|
// Always regenerate to reflect the current browsing context, not the original
|
|
544
619
|
// context
|
|
545
620
|
// (e.g., a track favorited from an album should use /favorites context when browsed
|
|
546
621
|
// there)
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
622
|
+
// The flat page position rides along as a duplicate-identity tie-breaker.
|
|
623
|
+
val trackIdentity = track.identity
|
|
624
|
+
if (track.src != null && trackIdentity != null) {
|
|
625
|
+
val contextualPath = BrowserPathHelper.build(path, trackIdentity, index)
|
|
626
|
+
transformedTrack = transformedTrack.copy(path = contextualPath)
|
|
550
627
|
|
|
551
628
|
Timber.d(
|
|
552
|
-
"[$path] Child[$index] '${track.title}': Playable,
|
|
629
|
+
"[$path] Child[$index] '${track.title}': Playable, contextualPath=$contextualPath (identity=$trackIdentity)"
|
|
553
630
|
)
|
|
554
631
|
} else {
|
|
555
632
|
Timber.d(
|
|
556
|
-
"[$path] Child[$index] '${track.title}': Browsable with
|
|
633
|
+
"[$path] Child[$index] '${track.title}': Browsable with path=${track.path}"
|
|
557
634
|
)
|
|
558
635
|
}
|
|
559
636
|
|
|
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
637
|
// Transform artwork URL. At browse-time there is no display size info.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
)
|
|
588
|
-
|
|
589
|
-
transformedTrack
|
|
638
|
+
transformArtworkUrl(
|
|
639
|
+
transformedTrack,
|
|
640
|
+
effectiveArtworkConfig,
|
|
641
|
+
path,
|
|
642
|
+
index,
|
|
643
|
+
ImageContext(null, null),
|
|
644
|
+
)
|
|
590
645
|
}
|
|
591
646
|
}
|
|
592
|
-
.awaitAll()
|
|
593
647
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
return if (transformedChildren != null) {
|
|
598
|
-
resolvedTrack.copy(children = transformedChildren.toTypedArray())
|
|
599
|
-
} else {
|
|
600
|
-
resolvedTrack
|
|
648
|
+
.map { (section, transformedChildren) ->
|
|
649
|
+
section.copy(children = transformedChildren.awaitAll().toTypedArray())
|
|
650
|
+
}
|
|
601
651
|
}
|
|
652
|
+
|
|
653
|
+
return resolvedTrack.copy(sections = transformedSections.toTypedArray(), children = null)
|
|
602
654
|
}
|
|
603
655
|
|
|
604
656
|
/**
|
|
@@ -657,55 +709,68 @@ class BrowserManager {
|
|
|
657
709
|
}
|
|
658
710
|
|
|
659
711
|
/**
|
|
660
|
-
* Expands a contextual
|
|
712
|
+
* Expands a contextual path into a queue of playable tracks.
|
|
661
713
|
*
|
|
662
714
|
* Used when navigating to a track to load it with its full album/playlist context. Returns only
|
|
663
715
|
* the selected track if singleTrack is true.
|
|
664
716
|
*
|
|
665
|
-
* @param
|
|
717
|
+
* @param contextualPath The contextual path (e.g., "/album?__trackId=song.mp3")
|
|
666
718
|
* @return Pair of (tracks array, selected track index), or null if expansion fails
|
|
667
719
|
*/
|
|
668
|
-
suspend fun
|
|
669
|
-
val trackId = BrowserPathHelper.extractTrackId(
|
|
720
|
+
suspend fun expandQueueFromContextualPath(contextualPath: String): Pair<Array<Track>, Int>? {
|
|
721
|
+
val trackId = BrowserPathHelper.extractTrackId(contextualPath) ?: return null
|
|
670
722
|
|
|
671
|
-
Timber.d("Expanding queue from contextual
|
|
723
|
+
Timber.d("Expanding queue from contextual path: $contextualPath (trackId=$trackId)")
|
|
672
724
|
|
|
673
725
|
try {
|
|
674
726
|
// Resolve the parent container to get all siblings
|
|
675
|
-
val parentPath = BrowserPathHelper.stripTrackId(
|
|
727
|
+
val parentPath = BrowserPathHelper.stripTrackId(contextualPath)
|
|
676
728
|
val parentResolvedTrack = resolve(parentPath)
|
|
677
|
-
val
|
|
729
|
+
val sections = parentResolvedTrack.normalizedSections
|
|
678
730
|
|
|
679
|
-
if (
|
|
731
|
+
if (sections.isNullOrEmpty()) {
|
|
680
732
|
Timber.w("Parent has no children, cannot expand queue")
|
|
681
733
|
return null
|
|
682
734
|
}
|
|
683
735
|
|
|
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
|
-
|
|
736
|
+
// Queue scope is the tapped section, not the whole page (ADR 0006). The
|
|
737
|
+
// stamped flat index pins which section (and which copy) was tapped when
|
|
738
|
+
// the same identity appears more than once; it is only a tie-breaker — a
|
|
739
|
+
// stale index falls back to the first identity match. An id that no
|
|
740
|
+
// longer appears on the page at all aborts the expansion — the caller
|
|
741
|
+
// falls back to the stored single track; silently queueing the changed
|
|
742
|
+
// list would resume the wrong station.
|
|
743
|
+
val tappedIndex = BrowserPathHelper.extractIndex(contextualPath)
|
|
744
|
+
val scoped = SectionScope.scoped(sections, trackId, tappedIndex) ?: return null
|
|
745
|
+
val sectionTracks = scoped.section.children.toList()
|
|
746
|
+
val tappedOffset = scoped.tappedOffset
|
|
747
|
+
|
|
748
|
+
// Filter to only playable tracks (tracks with src). A disabled track is
|
|
749
|
+
// unavailable — queue expansion excludes it, so auto-advance never meets
|
|
750
|
+
// one.
|
|
751
|
+
val playableTracks =
|
|
752
|
+
sectionTracks.filter { track -> track.src != null && track.disabled != true }
|
|
698
753
|
|
|
699
754
|
if (playableTracks.isEmpty()) {
|
|
700
755
|
Timber.w("Parent has no playable tracks, cannot expand queue")
|
|
701
756
|
return null
|
|
702
757
|
}
|
|
703
758
|
|
|
704
|
-
// Find the index of the selected track in the playable tracks array
|
|
705
|
-
|
|
759
|
+
// Find the index of the selected track in the playable tracks array:
|
|
760
|
+
// the pinned copy when the stamp survived, else the first identity match
|
|
761
|
+
val selectedIndex =
|
|
762
|
+
if (
|
|
763
|
+
tappedOffset != null &&
|
|
764
|
+
sectionTracks[tappedOffset].src != null &&
|
|
765
|
+
sectionTracks[tappedOffset].disabled != true
|
|
766
|
+
) {
|
|
767
|
+
sectionTracks.take(tappedOffset + 1).count { it.src != null && it.disabled != true } - 1
|
|
768
|
+
} else {
|
|
769
|
+
playableTracks.indexOfFirst { track -> track.identity == trackId }
|
|
770
|
+
}
|
|
706
771
|
|
|
707
772
|
if (selectedIndex < 0) {
|
|
708
|
-
Timber.w("Track with
|
|
773
|
+
Timber.w("Track with identity='$trackId' not found in playable children")
|
|
709
774
|
return null
|
|
710
775
|
}
|
|
711
776
|
|
|
@@ -716,11 +781,11 @@ class BrowserManager {
|
|
|
716
781
|
}
|
|
717
782
|
|
|
718
783
|
Timber.d(
|
|
719
|
-
"singleTrack=false - returning ${playableTracks.size} playable tracks (from ${
|
|
784
|
+
"singleTrack=false - returning ${playableTracks.size} playable tracks (from ${sectionTracks.size} in section), starting at index $selectedIndex"
|
|
720
785
|
)
|
|
721
786
|
return Pair(playableTracks.toTypedArray(), selectedIndex)
|
|
722
787
|
} catch (e: Exception) {
|
|
723
|
-
Timber.e(e, "Error expanding queue from contextual
|
|
788
|
+
Timber.e(e, "Error expanding queue from contextual path: $contextualPath")
|
|
724
789
|
return null
|
|
725
790
|
}
|
|
726
791
|
}
|
|
@@ -822,7 +887,10 @@ class BrowserManager {
|
|
|
822
887
|
*/
|
|
823
888
|
suspend fun searchPlayable(params: SearchParams): Array<Track>? {
|
|
824
889
|
val searchResults = search(params)
|
|
825
|
-
|
|
890
|
+
// Voice search never matches a disabled track (Track.disabled) — filtered
|
|
891
|
+
// before the first result is picked, so an unavailable one can't capture
|
|
892
|
+
// the play.
|
|
893
|
+
val tracks = searchResults.flattenedChildren?.filter { it.disabled != true }?.toTypedArray()
|
|
826
894
|
|
|
827
895
|
if (tracks.isNullOrEmpty()) {
|
|
828
896
|
return null
|
|
@@ -832,13 +900,13 @@ class BrowserManager {
|
|
|
832
900
|
|
|
833
901
|
// Check if result is browsable-only (container/route) vs playable
|
|
834
902
|
// If it's browsable but also playable (has src or playable=true), treat it as playable
|
|
835
|
-
val
|
|
903
|
+
val firstResultPath = firstResult.path
|
|
836
904
|
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 }
|
|
905
|
+
if (firstResult.src == null && firstResultPath != null) {
|
|
906
|
+
Timber.d("First search result is browsable-only, resolving: $firstResultPath")
|
|
907
|
+
val resolvedTrack = resolve(firstResultPath)
|
|
908
|
+
resolvedTrack.flattenedChildren
|
|
909
|
+
?.filter { it.src != null && it.disabled != true }
|
|
842
910
|
?.takeIf { it.isNotEmpty() }
|
|
843
911
|
?.toTypedArray() ?: tracks
|
|
844
912
|
} else {
|
|
@@ -883,34 +951,35 @@ class BrowserManager {
|
|
|
883
951
|
val searchPath = BrowserPathHelper.createSearchPath(params.query)
|
|
884
952
|
|
|
885
953
|
try {
|
|
886
|
-
// Execute search. Drop results without a stable identifier (
|
|
954
|
+
// Execute search. Drop results without a stable identifier (path or src): search results come
|
|
887
955
|
// from server/JS data that doesn't pass through validateTrack like browse children do, and
|
|
888
956
|
// downstream conversion (TrackFactory.toMedia3) and browsable-result resolution require one.
|
|
889
957
|
val searchResults =
|
|
890
958
|
resolveSearch(params)
|
|
891
959
|
.filter { track ->
|
|
892
|
-
val valid = track.
|
|
960
|
+
val valid = track.path != null || track.src != null
|
|
893
961
|
if (!valid) {
|
|
894
|
-
Timber.w("Dropping search result without
|
|
962
|
+
Timber.w("Dropping search result without path or src: '${track.title}'")
|
|
895
963
|
}
|
|
896
964
|
valid
|
|
897
965
|
}
|
|
898
966
|
.toTypedArray()
|
|
899
967
|
|
|
900
|
-
//
|
|
968
|
+
// Search results are a flat list; as a resolved page they are one
|
|
969
|
+
// untitled section (ADR 0010).
|
|
901
970
|
val searchResolvedTrack =
|
|
902
971
|
ResolvedTrack(
|
|
903
972
|
id = null,
|
|
904
|
-
|
|
973
|
+
path = searchPath,
|
|
905
974
|
title = "Search: ${params.query}",
|
|
906
|
-
|
|
975
|
+
sections = arrayOf(untitledSection(searchResults)),
|
|
976
|
+
children = null,
|
|
907
977
|
carPlaySiriListButton = null,
|
|
908
978
|
artwork = null,
|
|
909
979
|
artworkSource = null,
|
|
910
980
|
request = null,
|
|
911
|
-
artworkCarPlayTinted = null,
|
|
912
981
|
artist = null,
|
|
913
|
-
|
|
982
|
+
albumPath = null,
|
|
914
983
|
description = null,
|
|
915
984
|
subtitle = null,
|
|
916
985
|
album = null,
|
|
@@ -918,11 +987,9 @@ class BrowserManager {
|
|
|
918
987
|
duration = null,
|
|
919
988
|
src = null,
|
|
920
989
|
style = null,
|
|
921
|
-
|
|
990
|
+
disabled = null,
|
|
922
991
|
favorited = null,
|
|
923
|
-
groupTitle = null,
|
|
924
992
|
live = null,
|
|
925
|
-
imageRow = null,
|
|
926
993
|
)
|
|
927
994
|
|
|
928
995
|
// Cache search results for getCachedSearchResults()
|
|
@@ -942,16 +1009,16 @@ class BrowserManager {
|
|
|
942
1009
|
val emptySearchResult =
|
|
943
1010
|
ResolvedTrack(
|
|
944
1011
|
id = null,
|
|
945
|
-
|
|
1012
|
+
path = searchPath,
|
|
946
1013
|
title = "Search: ${params.query}",
|
|
947
|
-
|
|
1014
|
+
sections = arrayOf(untitledSection(emptyArray())),
|
|
1015
|
+
children = null,
|
|
948
1016
|
carPlaySiriListButton = null,
|
|
949
1017
|
artwork = null,
|
|
950
1018
|
artworkSource = null,
|
|
951
1019
|
request = null,
|
|
952
|
-
artworkCarPlayTinted = null,
|
|
953
1020
|
artist = null,
|
|
954
|
-
|
|
1021
|
+
albumPath = null,
|
|
955
1022
|
description = null,
|
|
956
1023
|
subtitle = null,
|
|
957
1024
|
album = null,
|
|
@@ -959,11 +1026,9 @@ class BrowserManager {
|
|
|
959
1026
|
duration = null,
|
|
960
1027
|
src = null,
|
|
961
1028
|
style = null,
|
|
962
|
-
|
|
1029
|
+
disabled = null,
|
|
963
1030
|
favorited = null,
|
|
964
|
-
groupTitle = null,
|
|
965
1031
|
live = null,
|
|
966
|
-
imageRow = null,
|
|
967
1032
|
)
|
|
968
1033
|
|
|
969
1034
|
return emptySearchResult
|
|
@@ -1040,7 +1105,7 @@ class BrowserManager {
|
|
|
1040
1105
|
// Validate tabs have stable identifiers
|
|
1041
1106
|
tabs.forEachIndexed { index, tab ->
|
|
1042
1107
|
validateTrack(tab, "Tab")
|
|
1043
|
-
Timber.d("[TABS] Tab[$index] '${tab.title}':
|
|
1108
|
+
Timber.d("[TABS] Tab[$index] '${tab.title}': path=${tab.path}")
|
|
1044
1109
|
}
|
|
1045
1110
|
|
|
1046
1111
|
this.tabs = tabs
|
|
@@ -1178,7 +1243,8 @@ class BrowserManager {
|
|
|
1178
1243
|
Timber.d("Resolving tabs via route entry")
|
|
1179
1244
|
val resolvedTrack = resolveRouteEntry(tabsEntry, TABS_ROUTE_PATH, emptyMap())
|
|
1180
1245
|
|
|
1181
|
-
|
|
1246
|
+
// Tabs are a flat list, not a page — a sectioned tabs source flattens.
|
|
1247
|
+
return resolvedTrack.flattenedChildren?.toTypedArray() ?: emptyArray()
|
|
1182
1248
|
}
|
|
1183
1249
|
|
|
1184
1250
|
/**
|