react-native-audio-browser 0.2.0-next.3 → 0.2.0-next.30

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.
Files changed (989) hide show
  1. package/README.md +36 -22
  2. package/android/build.gradle +35 -12
  3. package/android/gradle.properties +2 -2
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +541 -278
  6. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  8. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +52 -0
  9. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
  10. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +27 -9
  11. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +288 -0
  12. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
  13. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
  14. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
  15. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
  16. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  17. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  18. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  19. package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
  20. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
  21. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
  22. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  23. package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
  24. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
  25. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
  26. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  27. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
  28. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
  29. package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
  30. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  31. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
  32. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  33. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  34. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
  35. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
  36. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  37. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
  38. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
  39. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  40. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +62 -40
  41. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  42. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +110 -0
  43. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
  44. package/android/src/main/res/values/strings.xml +25 -5
  45. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +100 -0
  46. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
  47. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  48. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
  49. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
  50. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +40 -2
  51. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +301 -0
  52. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
  53. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +168 -0
  54. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
  55. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +160 -0
  56. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  57. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  58. package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
  59. package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
  60. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
  61. package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
  62. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  63. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  64. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  65. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  66. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  67. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  68. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
  69. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
  70. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  71. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
  72. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +55 -16
  73. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
  74. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  75. package/ios/AVPlayerEngine.swift +330 -0
  76. package/ios/AVPlayerEngineCallbacks.swift +15 -0
  77. package/ios/Browser/BrowseGate.swift +58 -0
  78. package/ios/Browser/BrowserConfig.swift +117 -78
  79. package/ios/Browser/BrowserManager.swift +209 -603
  80. package/ios/Browser/BrowserPathHelper.swift +50 -46
  81. package/ios/Browser/InertStyleDiagnostic.swift +296 -0
  82. package/ios/Browser/JsonModels.swift +264 -106
  83. package/ios/Browser/NavigationErrorState.swift +41 -0
  84. package/ios/Browser/PageResolver.swift +227 -0
  85. package/ios/Browser/RequestLayer.swift +90 -0
  86. package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +193 -121
  87. package/ios/Browser/SearchDrillIn.swift +7 -7
  88. package/ios/Browser/SectionScope.swift +58 -24
  89. package/ios/Browser/StyleResolver.swift +58 -0
  90. package/ios/Browser/TabBarEntries.swift +9 -3
  91. package/ios/Browser/TrackIdTemplate.swift +37 -0
  92. package/ios/CarPlay/CPInterfaceController+Safe.swift +100 -43
  93. package/ios/CarPlay/CarPlayArtworkResolver.swift +36 -83
  94. package/ios/CarPlay/CarPlayController.swift +42 -1296
  95. package/ios/CarPlay/CarPlayImageLoader.swift +78 -97
  96. package/ios/CarPlay/CarPlayListItemFactory.swift +303 -92
  97. package/ios/CarPlay/CarPlayNowPlayingManager.swift +129 -136
  98. package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
  99. package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
  100. package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
  101. package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
  102. package/ios/CarPlay/CarPlaySession.swift +209 -0
  103. package/ios/CarPlay/CarPlayTemplates.swift +63 -0
  104. package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
  105. package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +17 -9
  106. package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
  107. package/ios/CarPlay/RNABMediaAddHandler.swift +8 -24
  108. package/ios/CarPlay/RNABMediaAffinityHandler.swift +7 -23
  109. package/ios/CarPlay/RNABMediaIntentHandler.swift +18 -19
  110. package/ios/CarPlay/SectionPresentation.swift +77 -0
  111. package/ios/CarPlay/TopTemplateReplacer.swift +96 -0
  112. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  113. package/ios/Extension/NavigationError+Conversion.swift +14 -4
  114. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  115. package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
  116. package/ios/Extension/Section+Copying.swift +29 -0
  117. package/ios/Extension/String+IcyCharset.swift +21 -0
  118. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  119. package/ios/Extension/Track+Copying.swift +8 -12
  120. package/ios/Extension/Track+Identity.swift +18 -0
  121. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  122. package/ios/Extension/UIImage+Adaptive.swift +12 -0
  123. package/ios/Extension/URLError+Connectivity.swift +15 -0
  124. package/ios/Http/HttpClient.swift +66 -142
  125. package/ios/HybridAudioBrowser+Browser.swift +153 -0
  126. package/ios/HybridAudioBrowser+Options.swift +144 -0
  127. package/ios/HybridAudioBrowser+Player.swift +397 -0
  128. package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
  129. package/ios/HybridAudioBrowser+Stubs.swift +46 -0
  130. package/ios/HybridAudioBrowser.swift +153 -1610
  131. package/ios/Model/NitroTypeStubs.swift +146 -24
  132. package/ios/Model/PlayerUpdateOptions.swift +8 -1
  133. package/ios/Model/RemoteCommand.swift +18 -0
  134. package/ios/Model/TrackPlayerError.swift +2 -2
  135. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
  136. package/ios/Observer/ItemObserver.swift +90 -0
  137. package/ios/Observer/PlayerObserver.swift +59 -0
  138. package/ios/Option/SessionCategories.swift +42 -92
  139. package/ios/Player/AudioSessionController.swift +177 -0
  140. package/ios/Player/DeferredSeek.swift +70 -0
  141. package/ios/Player/ErrorStatus.swift +27 -0
  142. package/ios/Player/MediaLoader.swift +62 -35
  143. package/ios/Player/NowPlayingUpdater.swift +197 -50
  144. package/ios/Player/PlaybackFailure.swift +125 -0
  145. package/ios/Player/PlaybackPersistence.swift +73 -0
  146. package/ios/Player/PlaybackTimer.swift +8 -2
  147. package/ios/Player/Player.swift +1078 -0
  148. package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
  149. package/ios/Player/PlayerEngine.swift +87 -0
  150. package/ios/Player/PlayerStatusTypes.swift +2 -6
  151. package/ios/Player/QueueManager.swift +93 -67
  152. package/ios/Player/RetryLoop.swift +261 -0
  153. package/ios/Player/ShuffleOrder.swift +33 -0
  154. package/ios/Player/SleepTimerManager.swift +1 -1
  155. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +46 -0
  156. package/ios/RemoteCommand/RemoteCommandController.swift +87 -331
  157. package/ios/Tests/BrowserPathHelperTests.swift +56 -15
  158. package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -373
  159. package/ios/Tests/DeferredSeekTests.swift +70 -0
  160. package/ios/Tests/EmitterTests.swift +29 -0
  161. package/ios/Tests/GenerationMemoTests.swift +121 -0
  162. package/ios/Tests/InertStyleDiagnosticTests.swift +209 -0
  163. package/ios/Tests/JsonModelsTests.swift +164 -1
  164. package/ios/Tests/LRUCacheTests.swift +110 -0
  165. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  166. package/ios/Tests/MediaLoaderTests.swift +12 -48
  167. package/ios/Tests/ObserverTests.swift +88 -0
  168. package/ios/Tests/PlaybackErrorTests.swift +76 -0
  169. package/ios/Tests/PlaybackFailureTests.swift +189 -0
  170. package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
  171. package/ios/Tests/PlayerPolicyTests.swift +420 -0
  172. package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +300 -248
  173. package/ios/Tests/PlayingStateTests.swift +42 -0
  174. package/ios/Tests/QueueManagerTests.swift +196 -115
  175. package/ios/Tests/ReadyValueTests.swift +73 -0
  176. package/ios/Tests/RemoteCommandControllerTests.swift +152 -0
  177. package/ios/Tests/RemoteCommandTests.swift +65 -0
  178. package/ios/Tests/RequestLayerTests.swift +112 -0
  179. package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
  180. package/ios/Tests/RetryLoopTests.swift +270 -0
  181. package/ios/Tests/SearchDrillInTests.swift +16 -16
  182. package/ios/Tests/SectionPresentationTests.swift +98 -0
  183. package/ios/Tests/SectionScopeTests.swift +87 -55
  184. package/ios/Tests/StyleResolverTests.swift +132 -0
  185. package/ios/Tests/Support/TestTypes.swift +108 -104
  186. package/ios/Tests/TabBarEntriesTests.swift +19 -19
  187. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  188. package/ios/Tests/TopTemplateReplacerTests.swift +172 -0
  189. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  190. package/ios/Tests/TrackIdentityTests.swift +20 -0
  191. package/ios/Tests/TrackSelectorTests.swift +168 -33
  192. package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
  193. package/ios/TrackSelector.swift +46 -31
  194. package/ios/Util/ArtworkImageFetcher.swift +58 -1
  195. package/ios/Util/Emitter.swift +42 -0
  196. package/ios/Util/GenerationMemo.swift +36 -0
  197. package/ios/Util/LRUCache.swift +23 -159
  198. package/ios/Util/NetworkMonitor.swift +43 -26
  199. package/ios/Util/ReadyValue.swift +56 -0
  200. package/ios/Util/SFSymbolRenderer.swift +59 -31
  201. package/ios/Util/SVGProcessor.swift +9 -12
  202. package/lib/commonjs/AudioBrowser.js +8 -8
  203. package/lib/commonjs/AudioBrowser.js.map +1 -1
  204. package/lib/commonjs/CLAUDE.md +52 -0
  205. package/lib/commonjs/features/browser-config.js +12 -8
  206. package/lib/commonjs/features/browser-config.js.map +1 -1
  207. package/lib/commonjs/features/browser.js +11 -3
  208. package/lib/commonjs/features/browser.js.map +1 -1
  209. package/lib/commonjs/features/carConnection.js +1 -1
  210. package/lib/commonjs/features/equalizer.js +1 -1
  211. package/lib/commonjs/features/errors.js +3 -3
  212. package/lib/commonjs/features/favorites.js +14 -11
  213. package/lib/commonjs/features/favorites.js.map +1 -1
  214. package/lib/commonjs/features/gate.js +16 -1
  215. package/lib/commonjs/features/gate.js.map +1 -1
  216. package/lib/commonjs/features/metadata.js +20 -3
  217. package/lib/commonjs/features/metadata.js.map +1 -1
  218. package/lib/commonjs/features/network.js +1 -1
  219. package/lib/commonjs/features/nowPlaying.js +1 -1
  220. package/lib/commonjs/features/output.js +1 -1
  221. package/lib/commonjs/features/playback/controls.js +4 -2
  222. package/lib/commonjs/features/playback/controls.js.map +1 -1
  223. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  224. package/lib/commonjs/features/playback/playing.js +1 -1
  225. package/lib/commonjs/features/playback/progress.js +3 -2
  226. package/lib/commonjs/features/playback/progress.js.map +1 -1
  227. package/lib/commonjs/features/playback/rate.js +8 -1
  228. package/lib/commonjs/features/playback/rate.js.map +1 -1
  229. package/lib/commonjs/features/playback/state.js +3 -4
  230. package/lib/commonjs/features/playback/state.js.map +1 -1
  231. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  232. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  233. package/lib/commonjs/features/playback/volume.js +1 -1
  234. package/lib/commonjs/features/player/options.js +48 -34
  235. package/lib/commonjs/features/player/options.js.map +1 -1
  236. package/lib/commonjs/features/player/setup.js +18 -9
  237. package/lib/commonjs/features/player/setup.js.map +1 -1
  238. package/lib/commonjs/features/player/validateOptions.js +23 -0
  239. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  240. package/lib/commonjs/features/queue/activeTrack.js +15 -4
  241. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  242. package/lib/commonjs/features/queue/queue.js +13 -6
  243. package/lib/commonjs/features/queue/queue.js.map +1 -1
  244. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  245. package/lib/commonjs/features/queue/shuffle.js +1 -1
  246. package/lib/commonjs/features/remoteControls.js +13 -92
  247. package/lib/commonjs/features/remoteControls.js.map +1 -1
  248. package/lib/commonjs/features/sleepTimer.js +13 -1
  249. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  250. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  251. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  252. package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
  253. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  254. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  255. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  256. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  257. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  258. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  259. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  260. package/lib/commonjs/web/NativeAudioBrowser.js +201 -76
  261. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  262. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  263. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  264. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  265. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  266. package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
  267. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  268. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
  269. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  270. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  271. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  272. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  273. package/lib/commonjs/web/browser/BrowserManager.js +175 -100
  274. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  275. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  276. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  277. package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
  278. package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
  279. package/lib/commonjs/web/browser/SearchManager.js +18 -23
  280. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  281. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  282. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  283. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  284. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  285. package/lib/commonjs/web/browser/sections.js +89 -0
  286. package/lib/commonjs/web/browser/sections.js.map +1 -0
  287. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  288. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  289. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  290. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  291. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  292. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  293. package/lib/commonjs/web/util/BrowserPathHelper.js +62 -11
  294. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  295. package/lib/module/AudioBrowser.js +2 -1
  296. package/lib/module/AudioBrowser.js.map +1 -1
  297. package/lib/module/CLAUDE.md +52 -0
  298. package/lib/module/features/browser-config.js +12 -8
  299. package/lib/module/features/browser-config.js.map +1 -1
  300. package/lib/module/features/browser.js +11 -3
  301. package/lib/module/features/browser.js.map +1 -1
  302. package/lib/module/features/carConnection.js +1 -1
  303. package/lib/module/features/equalizer.js +1 -1
  304. package/lib/module/features/errors.js +3 -3
  305. package/lib/module/features/favorites.js +14 -11
  306. package/lib/module/features/favorites.js.map +1 -1
  307. package/lib/module/features/gate.js +16 -1
  308. package/lib/module/features/gate.js.map +1 -1
  309. package/lib/module/features/metadata.js +20 -3
  310. package/lib/module/features/metadata.js.map +1 -1
  311. package/lib/module/features/network.js +1 -1
  312. package/lib/module/features/nowPlaying.js +1 -1
  313. package/lib/module/features/output.js +1 -1
  314. package/lib/module/features/playback/controls.js +4 -2
  315. package/lib/module/features/playback/controls.js.map +1 -1
  316. package/lib/module/features/playback/playWhenReady.js +1 -1
  317. package/lib/module/features/playback/playing.js +1 -1
  318. package/lib/module/features/playback/progress.js +3 -2
  319. package/lib/module/features/playback/progress.js.map +1 -1
  320. package/lib/module/features/playback/rate.js +8 -1
  321. package/lib/module/features/playback/rate.js.map +1 -1
  322. package/lib/module/features/playback/state.js +3 -4
  323. package/lib/module/features/playback/state.js.map +1 -1
  324. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  325. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  326. package/lib/module/features/playback/volume.js +1 -1
  327. package/lib/module/features/player/options.js +47 -32
  328. package/lib/module/features/player/options.js.map +1 -1
  329. package/lib/module/features/player/setup.js +17 -8
  330. package/lib/module/features/player/setup.js.map +1 -1
  331. package/lib/module/features/player/validateOptions.js +19 -0
  332. package/lib/module/features/player/validateOptions.js.map +1 -0
  333. package/lib/module/features/queue/activeTrack.js +15 -4
  334. package/lib/module/features/queue/activeTrack.js.map +1 -1
  335. package/lib/module/features/queue/queue.js +13 -6
  336. package/lib/module/features/queue/queue.js.map +1 -1
  337. package/lib/module/features/queue/repeatMode.js +1 -1
  338. package/lib/module/features/queue/shuffle.js +1 -1
  339. package/lib/module/features/remoteControls.js +13 -92
  340. package/lib/module/features/remoteControls.js.map +1 -1
  341. package/lib/module/features/sleepTimer.js +13 -1
  342. package/lib/module/features/sleepTimer.js.map +1 -1
  343. package/lib/module/test-utils/reactNativeStub.js +58 -0
  344. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  345. package/lib/module/utils/LazyNativeEmitter.js +28 -14
  346. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  347. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  348. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  349. package/lib/module/utils/getTrackIdentity.js +20 -0
  350. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  351. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  352. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  353. package/lib/module/web/NativeAudioBrowser.js +201 -76
  354. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  355. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  356. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  357. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  358. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  359. package/lib/module/web/TrackPlayer/Player.js +254 -39
  360. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  361. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
  362. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  363. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  364. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  365. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  366. package/lib/module/web/browser/BrowserManager.js +175 -100
  367. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  368. package/lib/module/web/browser/FavoriteManager.js +31 -21
  369. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  370. package/lib/module/web/browser/NavigationErrorManager.js +3 -1
  371. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  372. package/lib/module/web/browser/SearchManager.js +18 -23
  373. package/lib/module/web/browser/SearchManager.js.map +1 -1
  374. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  375. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  376. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  377. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  378. package/lib/module/web/browser/sections.js +83 -0
  379. package/lib/module/web/browser/sections.js.map +1 -0
  380. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  381. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  382. package/lib/module/web/http/followMediaRedirect.js +215 -0
  383. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  384. package/lib/module/web/player/NowPlayingManager.js +4 -3
  385. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  386. package/lib/module/web/util/BrowserPathHelper.js +62 -11
  387. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  388. package/lib/typescript/package.json +1 -0
  389. package/lib/typescript/src/AudioBrowser.d.ts +1 -1
  390. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  391. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  392. package/lib/typescript/src/features/browser.d.ts +10 -2
  393. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  394. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  395. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  396. package/lib/typescript/src/features/errors.d.ts +3 -3
  397. package/lib/typescript/src/features/favorites.d.ts +14 -11
  398. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  399. package/lib/typescript/src/features/gate.d.ts +18 -2
  400. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  401. package/lib/typescript/src/features/metadata.d.ts +20 -3
  402. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  403. package/lib/typescript/src/features/network.d.ts +1 -1
  404. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  405. package/lib/typescript/src/features/output.d.ts +1 -1
  406. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  407. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  408. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  409. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  410. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  411. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  412. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  413. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  414. package/lib/typescript/src/features/playback/state.d.ts +3 -4
  415. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  416. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  417. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  418. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  419. package/lib/typescript/src/features/player/options.d.ts +108 -56
  420. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  421. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  422. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  423. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  424. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  425. package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
  426. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  427. package/lib/typescript/src/features/queue/queue.d.ts +7 -4
  428. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  429. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  430. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  431. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  432. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  433. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  434. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  435. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  436. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  437. package/lib/typescript/src/types/browser-native.d.ts +3 -2
  438. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  439. package/lib/typescript/src/types/browser-nodes.d.ts +387 -126
  440. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  441. package/lib/typescript/src/types/browser.d.ts +93 -61
  442. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  443. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
  444. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  445. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  446. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  447. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  448. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  449. package/lib/typescript/src/utils/resolveAssetSource.d.ts +1 -2
  450. package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
  451. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  452. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  453. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
  454. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +4 -2
  455. package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +2 -2
  456. package/nitrogen/generated/android/c++/JAndroidAudioOffloadSettings.hpp +2 -2
  457. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +12 -12
  458. package/nitrogen/generated/android/c++/JAndroidPlayerWakeMode.hpp +2 -2
  459. package/nitrogen/generated/android/c++/JAppKilledPlaybackBehavior.hpp +2 -2
  460. package/nitrogen/generated/android/c++/JArtworkRendering.hpp +58 -0
  461. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +10 -5
  462. package/nitrogen/generated/android/c++/JArtworkVariants.hpp +2 -2
  463. package/nitrogen/generated/android/c++/JBatteryOptimizationStatus.hpp +2 -2
  464. package/nitrogen/generated/android/c++/JBatteryOptimizationStatusChangedEvent.hpp +2 -2
  465. package/nitrogen/generated/android/c++/JBatteryWarningPendingChangedEvent.hpp +2 -2
  466. package/nitrogen/generated/android/c++/JBrowseError.hpp +2 -2
  467. package/nitrogen/generated/android/c++/JBrowseResult.hpp +19 -7
  468. package/nitrogen/generated/android/c++/JBrowserSourceCallbackParam.hpp +2 -2
  469. package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +2 -2
  470. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  471. package/nitrogen/generated/android/c++/JCarPlaySiriListButtonPosition.hpp +2 -2
  472. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  473. package/nitrogen/generated/android/c++/JChapterMetadata.hpp +2 -2
  474. package/nitrogen/generated/android/c++/JEqualizerSettings.hpp +10 -10
  475. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +10 -5
  476. package/nitrogen/generated/android/c++/JFormatNavigationErrorParams.hpp +11 -5
  477. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +10 -5
  478. package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +2 -2
  479. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +2 -2
  480. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  481. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +10 -5
  482. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.hpp +4 -2
  483. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +10 -5
  484. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +10 -5
  485. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +2 -2
  486. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.hpp +2 -2
  487. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  488. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +10 -5
  489. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +2 -2
  490. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +18 -12
  491. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +10 -4
  492. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  493. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +18 -6
  494. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
  495. package/nitrogen/generated/android/c++/JFunc_void_BatteryOptimizationStatusChangedEvent.hpp +2 -2
  496. package/nitrogen/generated/android/c++/JFunc_void_BatteryWarningPendingChangedEvent.hpp +2 -2
  497. package/nitrogen/generated/android/c++/JFunc_void_EqualizerSettings.hpp +2 -2
  498. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +10 -5
  499. package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +2 -2
  500. package/nitrogen/generated/android/c++/JFunc_void_NavigationErrorEvent.hpp +2 -2
  501. package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +2 -2
  502. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -12
  503. package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +2 -2
  504. package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -2
  505. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +10 -5
  506. package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -2
  507. package/nitrogen/generated/android/c++/JFunc_void_PlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  508. package/nitrogen/generated/android/c++/JFunc_void_PlaybackProgressUpdatedEvent.hpp +2 -2
  509. package/nitrogen/generated/android/c++/JFunc_void_PlaybackQueueEndedEvent.hpp +2 -2
  510. package/nitrogen/generated/android/c++/JFunc_void_PlayingState.hpp +2 -2
  511. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpBackwardEvent.hpp +2 -2
  512. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpForwardEvent.hpp +2 -2
  513. package/nitrogen/generated/android/c++/JFunc_void_RemotePlayIdEvent.hpp +2 -2
  514. package/nitrogen/generated/android/c++/JFunc_void_RemotePlaySearchEvent.hpp +2 -2
  515. package/nitrogen/generated/android/c++/JFunc_void_RemoteSeekEvent.hpp +2 -2
  516. package/nitrogen/generated/android/c++/JFunc_void_RemoteSkipEvent.hpp +2 -2
  517. package/nitrogen/generated/android/c++/JFunc_void_RepeatModeChangedEvent.hpp +2 -2
  518. package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +2 -2
  519. package/nitrogen/generated/android/c++/JFunc_void_TrackMetadata.hpp +2 -2
  520. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
  521. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +2 -2
  522. package/nitrogen/generated/android/c++/JFunc_void_std__optional_FormattedNavigationError_.hpp +2 -2
  523. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +17 -5
  524. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.hpp +2 -2
  525. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +2 -2
  526. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ChapterMetadata_.hpp +16 -16
  527. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +24 -18
  528. package/nitrogen/generated/android/c++/JGate.hpp +2 -2
  529. package/nitrogen/generated/android/c++/JGateDecision.hpp +2 -2
  530. package/nitrogen/generated/android/c++/JGateEvent.hpp +2 -2
  531. package/nitrogen/generated/android/c++/JGateReason.hpp +2 -2
  532. package/nitrogen/generated/android/c++/JGridTile.hpp +64 -0
  533. package/nitrogen/generated/android/c++/JHttpMethod.hpp +2 -2
  534. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +77 -53
  535. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +3 -3
  536. package/nitrogen/generated/android/c++/JIOSCategory.hpp +2 -2
  537. package/nitrogen/generated/android/c++/JIOSCategoryMode.hpp +2 -2
  538. package/nitrogen/generated/android/c++/JIOSCategoryOptions.hpp +2 -2
  539. package/nitrogen/generated/android/c++/JIOSCategoryPolicy.hpp +2 -2
  540. package/nitrogen/generated/android/c++/JIOSOptions.hpp +17 -11
  541. package/nitrogen/generated/android/c++/JImageContext.hpp +2 -2
  542. package/nitrogen/generated/android/c++/JImageQueryParams.hpp +2 -2
  543. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  544. package/nitrogen/generated/android/c++/JImageSource.hpp +2 -2
  545. package/nitrogen/generated/android/c++/JMediaReference.hpp +2 -2
  546. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +10 -5
  547. package/nitrogen/generated/android/c++/JMediaTransformParams.hpp +2 -2
  548. package/nitrogen/generated/android/c++/JNativeAndroidSetupOptions.hpp +2 -2
  549. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +48 -21
  550. package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +2 -2
  551. package/nitrogen/generated/android/c++/JNativeIOSSetupOptions.hpp +10 -10
  552. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +16 -4
  553. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +17 -14
  554. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +9 -12
  555. package/nitrogen/generated/android/c++/JNavigationError.hpp +2 -2
  556. package/nitrogen/generated/android/c++/JNavigationErrorEvent.hpp +2 -2
  557. package/nitrogen/generated/android/c++/JNavigationErrorKind.hpp +58 -0
  558. package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +2 -2
  559. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +12 -12
  560. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +17 -11
  561. package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +2 -2
  562. package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +2 -2
  563. package/nitrogen/generated/android/c++/JOptions.hpp +9 -12
  564. package/nitrogen/generated/android/c++/JOutput.hpp +2 -2
  565. package/nitrogen/generated/android/c++/JOutputType.hpp +2 -2
  566. package/nitrogen/generated/android/c++/JPlayback.hpp +2 -2
  567. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +10 -5
  568. package/nitrogen/generated/android/c++/JPlaybackError.hpp +2 -2
  569. package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -2
  570. package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +2 -2
  571. package/nitrogen/generated/android/c++/JPlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  572. package/nitrogen/generated/android/c++/JPlaybackProgressUpdatedEvent.hpp +2 -2
  573. package/nitrogen/generated/android/c++/JPlaybackQueueEndedEvent.hpp +2 -2
  574. package/nitrogen/generated/android/c++/JPlaybackState.hpp +2 -2
  575. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +7 -13
  576. package/nitrogen/generated/android/c++/JPlayingState.hpp +2 -2
  577. package/nitrogen/generated/android/c++/JProgress.hpp +2 -2
  578. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  579. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  580. package/nitrogen/generated/android/c++/JRemoteJumpBackwardEvent.hpp +2 -2
  581. package/nitrogen/generated/android/c++/JRemoteJumpForwardEvent.hpp +2 -2
  582. package/nitrogen/generated/android/c++/JRemotePlayIdEvent.hpp +2 -2
  583. package/nitrogen/generated/android/c++/JRemotePlaySearchEvent.hpp +2 -2
  584. package/nitrogen/generated/android/c++/JRemoteSeekEvent.hpp +2 -2
  585. package/nitrogen/generated/android/c++/JRemoteSkipEvent.hpp +2 -2
  586. package/nitrogen/generated/android/c++/JRepeatMode.hpp +2 -2
  587. package/nitrogen/generated/android/c++/JRepeatModeChangedEvent.hpp +2 -2
  588. package/nitrogen/generated/android/c++/JRequestConfig.hpp +2 -2
  589. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +67 -59
  590. package/nitrogen/generated/android/c++/JRetryConfig.hpp +2 -2
  591. package/nitrogen/generated/android/c++/JSearchMode.hpp +2 -2
  592. package/nitrogen/generated/android/c++/JSearchParams.hpp +2 -2
  593. package/nitrogen/generated/android/c++/JSection.hpp +116 -0
  594. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  595. package/nitrogen/generated/android/c++/JSleepTimer.hpp +4 -4
  596. package/nitrogen/generated/android/c++/JSleepTimerEndOfTrack.hpp +2 -2
  597. package/nitrogen/generated/android/c++/JSleepTimerTime.hpp +2 -2
  598. package/nitrogen/generated/android/c++/JStallReason.hpp +2 -2
  599. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  600. package/nitrogen/generated/android/c++/JTimedMetadata.hpp +2 -2
  601. package/nitrogen/generated/android/c++/JTrack.hpp +27 -48
  602. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +18 -12
  603. package/nitrogen/generated/android/c++/JTrackMetadata.hpp +2 -2
  604. package/nitrogen/generated/android/c++/JTrackRequest.hpp +2 -2
  605. package/nitrogen/generated/android/c++/JTrackStyle.hpp +47 -19
  606. package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +2 -2
  607. package/nitrogen/generated/android/c++/JVariant_Boolean_AndroidAudioOffloadSettings.hpp +3 -3
  608. package/nitrogen/generated/android/c++/JVariant_Boolean_RetryConfig.hpp +3 -3
  609. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
  610. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  611. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  612. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +20 -8
  613. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +3 -3
  614. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioOffloadSettings.kt +15 -0
  615. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +20 -3
  616. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  617. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +37 -0
  618. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +15 -0
  619. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryOptimizationStatusChangedEvent.kt +13 -0
  620. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryWarningPendingChangedEvent.kt +13 -0
  621. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseError.kt +13 -0
  622. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseResult.kt +16 -7
  623. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowserSourceCallbackParam.kt +15 -0
  624. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  625. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  626. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ChapterMetadata.kt +19 -0
  627. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/EqualizerSettings.kt +27 -0
  628. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteChangedEvent.kt +15 -0
  629. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNavigationErrorParams.kt +25 -3
  630. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +21 -0
  631. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +15 -0
  632. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +0 -2
  633. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  634. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +0 -2
  635. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.kt +0 -2
  636. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +0 -2
  637. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +0 -2
  638. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +0 -2
  639. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.kt +0 -2
  640. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  641. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.kt +0 -2
  642. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +0 -2
  643. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.kt +0 -2
  644. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.kt +0 -2
  645. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +78 -0
  646. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt +0 -2
  647. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void.kt +0 -2
  648. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryOptimizationStatusChangedEvent.kt +0 -2
  649. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryWarningPendingChangedEvent.kt +0 -2
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_EqualizerSettings.kt +0 -2
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_FavoriteChangedEvent.kt +0 -2
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_GateEvent.kt +0 -2
  653. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NavigationErrorEvent.kt +0 -2
  654. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NowPlayingMetadata.kt +0 -2
  655. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Options.kt +0 -2
  656. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Output.kt +0 -2
  657. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Playback.kt +0 -2
  658. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackActiveTrackChangedEvent.kt +0 -2
  659. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackErrorEvent.kt +0 -2
  660. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackPlayWhenReadyChangedEvent.kt +0 -2
  661. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackProgressUpdatedEvent.kt +0 -2
  662. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackQueueEndedEvent.kt +0 -2
  663. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlayingState.kt +0 -2
  664. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpBackwardEvent.kt +0 -2
  665. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpForwardEvent.kt +0 -2
  666. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlayIdEvent.kt +0 -2
  667. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlaySearchEvent.kt +0 -2
  668. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSeekEvent.kt +0 -2
  669. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSkipEvent.kt +0 -2
  670. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RepeatModeChangedEvent.kt +0 -2
  671. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TimedMetadata.kt +0 -2
  672. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TrackMetadata.kt +0 -2
  673. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +0 -2
  674. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_double.kt +0 -2
  675. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_FormattedNavigationError_.kt +0 -2
  676. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_ResolvedTrack_.kt +0 -2
  677. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.kt +0 -2
  678. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__string.kt +0 -2
  679. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_ChapterMetadata_.kt +0 -2
  680. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_Track_.kt +0 -2
  681. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Gate.kt +15 -0
  682. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateDecision.kt +15 -0
  683. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateEvent.kt +13 -0
  684. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +25 -0
  685. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +3 -1
  686. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +25 -3
  687. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageContext.kt +15 -0
  688. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageQueryParams.kt +15 -0
  689. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  690. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageSource.kt +19 -0
  691. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +35 -0
  692. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaTransformParams.kt +15 -0
  693. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeAndroidSetupOptions.kt +31 -0
  694. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +51 -5
  695. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeGateRequest.kt +17 -0
  696. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeIOSSetupOptions.kt +21 -0
  697. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +27 -0
  698. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +29 -0
  699. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +23 -0
  700. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationError.kt +19 -0
  701. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorEvent.kt +13 -0
  702. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorKind.kt +23 -0
  703. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +20 -3
  704. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +25 -3
  705. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +29 -0
  706. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +17 -0
  707. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +23 -0
  708. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Output.kt +17 -0
  709. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Playback.kt +15 -0
  710. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackActiveTrackChangedEvent.kt +21 -0
  711. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +21 -0
  712. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorEvent.kt +13 -0
  713. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackPlayWhenReadyChangedEvent.kt +13 -0
  714. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackProgressUpdatedEvent.kt +19 -0
  715. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackQueueEndedEvent.kt +15 -0
  716. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +37 -2
  717. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayingState.kt +15 -0
  718. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Progress.kt +17 -0
  719. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  720. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +61 -0
  721. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpBackwardEvent.kt +13 -0
  722. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpForwardEvent.kt +13 -0
  723. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlayIdEvent.kt +15 -0
  724. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlaySearchEvent.kt +13 -0
  725. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSeekEvent.kt +13 -0
  726. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSkipEvent.kt +13 -0
  727. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RepeatModeChangedEvent.kt +13 -0
  728. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RequestConfig.kt +27 -0
  729. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +67 -15
  730. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +17 -0
  731. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +27 -0
  732. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +66 -0
  733. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +86 -0
  734. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimer.kt +18 -8
  735. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerEndOfTrack.kt +13 -0
  736. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerTime.kt +13 -0
  737. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  738. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TimedMetadata.kt +21 -0
  739. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +55 -12
  740. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackLoadEvent.kt +17 -0
  741. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackMetadata.kt +43 -0
  742. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +17 -0
  743. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +58 -5
  744. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +31 -0
  745. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_AndroidAudioOffloadSettings.kt +16 -7
  746. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_RetryConfig.kt +16 -7
  747. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_NullType_Double.kt +16 -7
  748. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +24 -15
  749. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +18 -8
  750. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +16 -7
  751. package/nitrogen/generated/ios/AudioBrowser+autolinking.rb +2 -0
  752. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +38 -29
  753. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +779 -665
  754. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +34 -16
  755. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +36 -18
  756. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  757. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  758. package/nitrogen/generated/ios/swift/BrowseResult.swift +12 -0
  759. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  760. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  761. package/nitrogen/generated/ios/swift/FormatNavigationErrorParams.swift +7 -2
  762. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  763. package/nitrogen/generated/ios/swift/GridTile.swift +48 -0
  764. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  765. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  766. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  767. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  768. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
  769. package/nitrogen/generated/ios/swift/NavigationErrorKind.swift +40 -0
  770. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  771. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  772. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  773. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  774. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  775. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
  776. package/nitrogen/generated/ios/swift/Section.swift +82 -0
  777. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  778. package/nitrogen/generated/ios/swift/SleepTimer.swift +13 -0
  779. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  780. package/nitrogen/generated/ios/swift/Track.swift +28 -63
  781. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  782. package/nitrogen/generated/ios/swift/Variant_Bool_AndroidAudioOffloadSettings.swift +12 -0
  783. package/nitrogen/generated/ios/swift/Variant_Bool_RetryConfig.swift +12 -0
  784. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +12 -0
  785. package/nitrogen/generated/ios/swift/Variant_NullType_RemoteButtonLayout.swift +30 -0
  786. package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +13 -0
  787. package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +12 -0
  788. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  789. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  790. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  791. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  792. package/nitrogen/generated/shared/c++/FormatNavigationErrorParams.hpp +9 -2
  793. package/nitrogen/generated/shared/c++/GridTile.hpp +84 -0
  794. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  795. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  796. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  797. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  798. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
  799. package/nitrogen/generated/shared/c++/NavigationErrorKind.hpp +76 -0
  800. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  801. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  802. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  803. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  804. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  805. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
  806. package/nitrogen/generated/shared/c++/Section.hpp +102 -0
  807. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  808. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  809. package/nitrogen/generated/shared/c++/Track.hpp +18 -30
  810. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  811. package/package.json +24 -11
  812. package/src/AudioBrowser.ts +2 -1
  813. package/src/CLAUDE.md +52 -0
  814. package/src/features/battery.test.ts +79 -0
  815. package/src/features/browser-config.test.ts +21 -6
  816. package/src/features/browser-config.ts +12 -8
  817. package/src/features/browser.test.ts +1 -1
  818. package/src/features/browser.ts +11 -3
  819. package/src/features/carConnection.ts +1 -1
  820. package/src/features/equalizer.ts +1 -1
  821. package/src/features/errors.ts +3 -3
  822. package/src/features/favorites.ts +14 -11
  823. package/src/features/gate.ts +16 -1
  824. package/src/features/hook-wiring.test.ts +302 -0
  825. package/src/features/metadata.ts +20 -3
  826. package/src/features/network.ts +1 -1
  827. package/src/features/nowPlaying.ts +1 -1
  828. package/src/features/output.ts +1 -1
  829. package/src/features/playback/controls.ts +4 -2
  830. package/src/features/playback/playWhenReady.ts +1 -1
  831. package/src/features/playback/playing.ts +1 -1
  832. package/src/features/playback/progress.test.ts +169 -0
  833. package/src/features/playback/progress.ts +3 -2
  834. package/src/features/playback/rate.ts +8 -1
  835. package/src/features/playback/state.ts +3 -4
  836. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  837. package/src/features/playback/trackPlaybackTime.ts +1 -6
  838. package/src/features/playback/volume.ts +1 -1
  839. package/src/features/player/options.ts +126 -76
  840. package/src/features/player/setup.test.ts +24 -7
  841. package/src/features/player/setup.ts +22 -10
  842. package/src/features/player/validateOptions.ts +21 -0
  843. package/src/features/queue/activeTrack.ts +15 -4
  844. package/src/features/queue/queue.ts +13 -6
  845. package/src/features/queue/repeatMode.ts +1 -1
  846. package/src/features/queue/shuffle.ts +1 -1
  847. package/src/features/remoteControls.ts +13 -92
  848. package/src/features/sleepTimer.test.ts +172 -0
  849. package/src/features/sleepTimer.ts +9 -1
  850. package/src/nitro-stub-drift.test.ts +277 -0
  851. package/src/public-surface.test.ts +147 -0
  852. package/src/specs/audio-browser.nitro.ts +5 -4
  853. package/src/style-resolution.test.ts +210 -0
  854. package/src/test-utils/reactNativeStub.ts +60 -0
  855. package/src/types/browser-native.ts +7 -3
  856. package/src/types/browser-nodes.ts +403 -134
  857. package/src/types/browser.ts +97 -63
  858. package/src/utils/LazyNativeEmitter.ts +28 -15
  859. package/src/utils/NativeUpdatedValue.ts +29 -13
  860. package/src/utils/getTrackIdentity.ts +20 -0
  861. package/src/utils/useDebug.test.ts +182 -0
  862. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  863. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  864. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  865. package/src/web/NativeAudioBrowser.ts +240 -83
  866. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  867. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  868. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  869. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  870. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  871. package/src/web/TrackPlayer/Player.ts +284 -37
  872. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
  873. package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
  874. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  875. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  876. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  877. package/src/web/__tests__/lifecycle.test.ts +345 -0
  878. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  879. package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
  880. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  881. package/src/web/__tests__/retry.test.ts +169 -0
  882. package/src/web/browser/BrowserManager.ts +209 -111
  883. package/src/web/browser/FavoriteManager.test.ts +84 -0
  884. package/src/web/browser/FavoriteManager.ts +28 -23
  885. package/src/web/browser/NavigationErrorManager.ts +3 -1
  886. package/src/web/browser/SearchManager.test.ts +75 -0
  887. package/src/web/browser/SearchManager.ts +25 -28
  888. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  889. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  890. package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
  891. package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
  892. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  893. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  894. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  895. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  896. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  897. package/src/web/browser/sections.test.ts +91 -0
  898. package/src/web/browser/sections.ts +72 -0
  899. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  900. package/src/web/http/RequestConfigBuilder.ts +140 -33
  901. package/src/web/http/followMediaRedirect.test.ts +289 -0
  902. package/src/web/http/followMediaRedirect.ts +253 -0
  903. package/src/web/player/NowPlayingManager.ts +4 -3
  904. package/src/web/util/BrowserPathHelper.test.ts +68 -0
  905. package/src/web/util/BrowserPathHelper.ts +70 -13
  906. package/website/guide/android-auto.md +114 -0
  907. package/website/guide/android-certificates.md +409 -0
  908. package/website/guide/artwork.md +255 -0
  909. package/website/guide/audio-output.md +135 -0
  910. package/website/guide/automotive.md +86 -0
  911. package/website/guide/basic-usage.md +151 -0
  912. package/website/guide/battery.md +115 -0
  913. package/website/guide/browser.md +587 -0
  914. package/website/guide/carplay.md +650 -0
  915. package/website/guide/configuration.md +173 -0
  916. package/website/guide/equalizer.md +231 -0
  917. package/website/guide/errors.md +310 -0
  918. package/website/guide/favorites.md +154 -0
  919. package/website/guide/gate.md +143 -0
  920. package/website/guide/getting-started.md +46 -0
  921. package/website/guide/hooks.md +238 -0
  922. package/website/guide/live-streams.md +125 -0
  923. package/website/guide/metadata.md +239 -0
  924. package/website/guide/migrating-from-track-player.md +244 -0
  925. package/website/guide/native-callback-fetch.md +127 -0
  926. package/website/guide/network.md +86 -0
  927. package/website/guide/now-playing.md +175 -0
  928. package/website/guide/playback.md +328 -0
  929. package/website/guide/queue.md +300 -0
  930. package/website/guide/remote-controls.md +256 -0
  931. package/website/guide/search.md +255 -0
  932. package/website/guide/sleep-timer.md +268 -0
  933. package/website/guide/track.md +247 -0
  934. package/ios/Bridge.h +0 -8
  935. package/ios/Browser/MediaResolveComposer.swift +0 -114
  936. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  937. package/ios/Model/MediaURL.swift +0 -67
  938. package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
  939. package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
  940. package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
  941. package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
  942. package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
  943. package/ios/Observer/PlayerStateObserver.swift +0 -50
  944. package/ios/Observer/PlayerTimeObserver.swift +0 -55
  945. package/ios/Option/PitchAlgorithms.swift +0 -17
  946. package/ios/Option/TimeEventFrequency.swift +0 -18
  947. package/ios/Player/LoadSeekCoordinator.swift +0 -99
  948. package/ios/Player/MediaLoaderDelegate.swift +0 -11
  949. package/ios/Player/PlaybackCoordinator.swift +0 -754
  950. package/ios/Player/PlaybackEffectHandler.swift +0 -54
  951. package/ios/Player/PlaybackErrorHandler.swift +0 -175
  952. package/ios/Player/PlaybackStateStore.swift +0 -27
  953. package/ios/Player/PlayingStateManager.swift +0 -33
  954. package/ios/Player/RetryManager.swift +0 -322
  955. package/ios/Player/SeekCompletionHandler.swift +0 -3
  956. package/ios/Player/SleepTimerHandling.swift +0 -15
  957. package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
  958. package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -325
  959. package/ios/Tests/MediaResolveTests.swift +0 -126
  960. package/ios/Tests/OnceValueTests.swift +0 -79
  961. package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -547
  962. package/ios/Tests/PlaybackStateStoreTests.swift +0 -48
  963. package/ios/Tests/PlayingStateManagerTests.swift +0 -57
  964. package/ios/Tests/RetryManagerTests.swift +0 -89
  965. package/ios/TrackPlayer.swift +0 -1014
  966. package/ios/TrackPlayerCallbacks.swift +0 -86
  967. package/ios/Util/MetadataAdapter.swift +0 -131
  968. package/ios/Util/OnceValue.swift +0 -104
  969. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  970. package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +0 -58
  971. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  972. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  973. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  974. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  975. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  976. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  977. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteConfig.kt +0 -38
  978. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  979. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  980. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  981. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  982. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  983. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  984. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  985. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  986. package/nitrogen/generated/ios/swift/Variant_NullType_NotificationButtonLayout.swift +0 -18
  987. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  988. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
  989. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -3,89 +3,49 @@ import NitroModules
3
3
  import os.log
4
4
 
5
5
  /// Custom browser errors.
6
- enum BrowserError: Error {
6
+ enum BrowserError: LocalizedError {
7
7
  case contentNotFound(path: String)
8
- case httpError(code: Int, body: String)
9
- case networkError(Error)
10
8
  case invalidConfiguration(String)
11
9
  case callbackError(String)
12
10
 
13
- var localizedDescription: String {
11
+ var errorDescription: String? {
14
12
  switch self {
15
- case .contentNotFound:
16
- return "Content not found"
17
- case let .httpError(code, body):
18
- // Try to extract error message from JSON response: { "error": "message" }
19
- if let data = body.data(using: .utf8),
20
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
21
- let errorMessage = json["error"] as? String
22
- {
23
- return errorMessage
24
- }
25
- // Fall back to localized HTTP status description
26
- return HTTPURLResponse.localizedString(forStatusCode: code)
27
- case let .networkError(error):
28
- return "Network error: \(error.localizedDescription)"
29
- case let .invalidConfiguration(message):
30
- return "Invalid configuration: \(message)"
31
- case let .callbackError(message):
32
- return message
13
+ case .contentNotFound: "Content not found"
14
+ case let .invalidConfiguration(message): "Invalid configuration: \(message)"
15
+ case let .callbackError(message): message
33
16
  }
34
17
  }
35
18
  }
36
19
 
37
- /// Core browser manager that handles navigation, search, and media browsing.
38
- ///
39
- /// This class contains the main business logic for:
40
- /// - Route resolution and path matching with parameter extraction
41
- /// - HTTP API requests and response processing
42
- /// - JavaScript callback invocation
43
- /// - Fallback handling and error management
20
+ /// The browser's state (path, content, tabs, config), its content and search caches, favorites
21
+ /// and events. Pages come from `PageResolver`; requests go through `RequestPipeline`.
44
22
  @MainActor
45
23
  final class BrowserManager {
46
24
  // Allow creation from nonisolated contexts (e.g. HybridAudioBrowser property default)
47
- nonisolated init() {}
25
+ nonisolated init() {
26
+ pages = PageResolver(requests: requests)
27
+ }
48
28
 
49
29
  let logger = Logger(subsystem: "com.audiobrowser", category: "BrowserManager")
50
30
 
51
- // MARK: - Constants (match Kotlin companion object)
52
-
53
- /// Internal path used for the default/root browse source
54
- static let defaultRoutePath = "__default__"
55
-
56
- /// Internal path used for navigation tabs
57
- static let tabsRoutePath = "__tabs__"
58
-
59
- /// Internal path used for search
60
- static let searchRoutePath = "__search__"
61
-
62
31
  // MARK: - Private Properties
63
32
 
64
- private let router = SimpleRouter()
65
- private let httpClient = HttpClient()
66
-
67
- // LRU cache for individual tracks - keyed by both url and src for O(1) lookup
68
- private let trackCache = LRUCache<String, Track>(maxSize: 3000)
33
+ let requests = RequestPipeline()
34
+ private let pages: PageResolver
69
35
 
70
36
  // LRU cache for resolved content - keyed by path
71
37
  private let contentCache = LRUCache<String, ResolvedTrack>(maxSize: 20)
72
38
 
73
- // Cache for search results
74
- private var lastSearchQuery: String?
39
+ // Most recent search, keyed by `searchCacheKey`.
40
+ private var lastSearchKey: String?
75
41
  private var lastSearchResults: [Track]?
76
42
 
77
- // Resolver caching: request/browse thunks resolve once per content generation.
78
- private var layerGeneration = 0
79
- private var resolvedLayerGeneration = -1
80
- var resolvedRequestLayer: TransformableRequestConfig?
81
- private var resolvedBrowseLayer: TransformableRequestConfig?
82
-
83
- // Set of favorited track identifiers (src)
43
+ // Set of favorited track identities (id when non-blank, else src).
84
44
  private var favoriteIds: Set<String> = []
85
45
 
86
- // Favorite match mode, propagated from the player's `favorite` capability.
87
- // nil = favoriting disabled (no row hearts). Set via setFavoriteMatch.
88
- private var favoriteMatch: FavoritesMatchMode?
46
+ // Whether favoriting is enabled, propagated from the player's `favorite`
47
+ // capability. false = no row hearts. Set via setFavoriteEnabled.
48
+ private var favoriteEnabled = false
89
49
 
90
50
  // Navigation tracking to prevent race conditions
91
51
  private var currentNavigationId: Int = 0
@@ -95,7 +55,7 @@ final class BrowserManager {
95
55
  private(set) var path: String = "/" {
96
56
  didSet {
97
57
  if oldValue != path {
98
- onPathChanged?(path)
58
+ pathChanged.emit(path)
99
59
  }
100
60
  }
101
61
  }
@@ -103,7 +63,7 @@ final class BrowserManager {
103
63
  private(set) var content: ResolvedTrack? {
104
64
  didSet {
105
65
  // Note: Can't compare ResolvedTrack directly, always fire callback
106
- onContentChanged?(content)
66
+ contentChanged.emit(content)
107
67
  }
108
68
  }
109
69
 
@@ -114,7 +74,7 @@ final class BrowserManager {
114
74
  // titles in a new locale), and an unchanged result must not churn the
115
75
  // CarPlay tab bar or reset its selection.
116
76
  if let tabs, !TabBarEntries.same(oldValue, tabs) {
117
- onTabsChanged?(tabs)
77
+ tabsChanged.emit(tabs)
118
78
  }
119
79
  }
120
80
  }
@@ -124,35 +84,38 @@ final class BrowserManager {
124
84
  /// Whether configureBrowser() has been called
125
85
  private(set) var isConfigured = false
126
86
 
127
- /// Browser configuration containing routes, search, tabs, and request settings.
128
- var config: BrowserConfig = .init() {
129
- didSet {
87
+ /// Browser configuration containing routes, search, tabs, and request settings. Kept on
88
+ /// `requests`, which every request reads.
89
+ var config: BrowserConfig {
90
+ get { requests.config }
91
+ set {
130
92
  isConfigured = true
131
- // New config means the cached resolver results are stale.
132
- layerGeneration += 1
133
- onConfigChanged?(config)
93
+ requests.config = newValue
94
+ configChanged.emit(newValue)
134
95
  }
135
96
  }
136
97
 
137
- // MARK: - Callbacks
98
+ // MARK: - Events (emitted on the main actor)
138
99
 
139
- var onPathChanged: ((String) -> Void)?
140
- var onContentChanged: ((ResolvedTrack?) -> Void)?
141
- var onTabsChanged: (([Track]) -> Void)?
142
- var onConfigChanged: ((BrowserConfig) -> Void)?
143
- /// Fired when the favorite id set changes (e.g. an app/webview-originated
144
- /// favorite). Lets surfaces like the CarPlay now-playing heart re-render —
145
- /// they otherwise only learn of their own toggles via favorite/active-track
146
- /// events, never an external change routed through `setFavorites`.
147
- var onFavoritesChanged: (() -> Void)?
100
+ let pathChanged = Emitter<String>()
101
+ let contentChanged = Emitter<ResolvedTrack?>()
102
+ let tabsChanged = Emitter<[Track]>()
103
+ let configChanged = Emitter<BrowserConfig>()
104
+ /// The favorite id set changed, however it changed (e.g. from the app or a webview), so
105
+ /// surfaces like the CarPlay now-playing heart can re-render.
106
+ let favoritesChanged = Emitter<Void>()
148
107
 
149
- /// Forwards to config.awaitTrackLoadHandler so callers don't need to
150
- /// cross the MainActor boundary to access `config`.
108
+ /// Runs the app's `handleTrackLoad` for `event` and waits for it, both promise layers: a JS
109
+ /// callback returning `Promise<void>` arrives as `Promise<Promise<Void>>`. False when the app
110
+ /// set none.
151
111
  func awaitTrackLoadHandler(event: TrackLoadEvent) async -> Bool {
152
- // awaitTrackLoadHandler manages its own MainActor dispatch internally
153
- nonisolated(unsafe) let cfg = config
154
- nonisolated(unsafe) let evt = event
155
- return await cfg.awaitTrackLoadHandler(event: evt)
112
+ guard let handler = config.handleTrackLoad else { return false }
113
+ do {
114
+ try await handler(event).await().await()
115
+ } catch {
116
+ logger.error("handleTrackLoad failed: \(error.localizedDescription)")
117
+ }
118
+ return true
156
119
  }
157
120
 
158
121
  // MARK: - Favorites
@@ -162,103 +125,75 @@ final class BrowserManager {
162
125
  let newIds = Set(favorites)
163
126
  guard newIds != favoriteIds else { return }
164
127
  favoriteIds = newIds
165
- onFavoritesChanged?()
128
+ favoritesChanged.emit(())
166
129
  }
167
130
 
168
- /// Sets the favorite match mode (propagated from the `favorite` capability).
169
- /// nil disables row-heart hydration.
170
- func setFavoriteMatch(_ match: FavoritesMatchMode?) {
171
- favoriteMatch = match
131
+ /// Enables/disables favoriting (propagated from the `favorite` capability).
132
+ /// false disables row-heart hydration.
133
+ func setFavoriteEnabled(_ enabled: Bool) {
134
+ favoriteEnabled = enabled
172
135
  }
173
136
 
174
- /// Optimistically reflects a local favorite toggle for `src` in the match set,
175
- /// needing no consumer-specific id extraction: favoriting inserts `src` (which
176
- /// matches itself under either mode). Removal mirrors `isFavorite`'s match
177
- /// semantics — partial drops every id that is a path segment of `src` (the
178
- /// channel uid, and self-heals stray ids); exact (or disabled) drops only
179
- /// `src`, so unrelated exact favorites that happen to be a segment of `src`
180
- /// aren't collaterally lost. The consumer reconciles `favoriteIds` to its
181
- /// canonical ids on the next `setFavorites`; `isFavorite(src:)` gives the same
182
- /// answer before and after, so the now-playing heart stays responsive.
183
- func setFavorited(src: String, favorited: Bool) {
137
+ /// Optimistically reflects a local favorite toggle in the favorite set, keyed
138
+ /// by the track's identity (id when non-blank, else src). The consumer
139
+ /// reconciles `favoriteIds` to its canonical set on the next `setFavorites`;
140
+ /// `hydrateFavorite` gives the same answer before and after, so the
141
+ /// now-playing heart stays responsive.
142
+ func setFavorited(identity: String, favorited: Bool) {
184
143
  if favorited {
185
- favoriteIds.insert(src)
186
- } else if favoriteMatch == .partial {
187
- favoriteIds = favoriteIds.filter { !BrowserPathHelper.containsSegment(src, $0) }
144
+ favoriteIds.insert(identity)
188
145
  } else {
189
- favoriteIds.remove(src)
146
+ favoriteIds.remove(identity)
190
147
  }
191
148
  }
192
149
 
193
150
  /// Hydrates the favorited field on a track based on the favoriteIds set.
194
151
  /// No-op unless favoriting is enabled (the `favorite` capability). Only
195
- /// playable (src-bearing) tracks are favoritable; the flag is set to true OR
196
- /// false so non-favorited tracks still show an (empty) heart. Local
197
- /// favoriteIds take precedence over API-provided values.
152
+ /// identity-bearing tracks (id or src) are favoritable; the flag is set to
153
+ /// true OR false so non-favorited tracks still show an (empty) heart. A
154
+ /// caller-set `favorited` wins — API/consumer-provided values are never
155
+ /// overwritten (matches Android and web).
198
156
  func hydrateFavorite(_ track: Track) -> Track {
199
- guard let match = favoriteMatch, let src = track.src else { return track }
200
-
201
- let isFavorited = isFavorite(src: src, match: match)
202
-
203
- // Only create a new track if the favorited state differs
204
- if track.favorited == isFavorited { return track }
205
-
206
- return track.copying(favorited: isFavorited)
157
+ // `identity` bridges a std::string out of the C++ track, so it is bound
158
+ // once here rather than computed again to answer "is it favorited".
159
+ guard favoriteEnabled, track.favorited == nil, let identity = track.identity else { return track }
160
+ return track.copying(favorited: favoriteIds.contains(identity))
207
161
  }
208
162
 
209
- /// Whether `src` is favorited under the given match mode.
210
- private func isFavorite(src: String, match: FavoritesMatchMode) -> Bool {
211
- switch match {
212
- case .exact:
213
- favoriteIds.contains(src)
214
- case .partial:
215
- favoriteIds.contains { BrowserPathHelper.containsSegment(src, $0) }
216
- }
217
- }
218
-
219
- /// Hydrates favorites on all children of a ResolvedTrack.
163
+ /// Hydrates favorites on all tracks of a resolved page. Pages reach
164
+ /// hydration normalized (ADR 0010), so `sections` is the only structure
165
+ /// to walk.
220
166
  private func hydrateChildren(_ resolvedTrack: ResolvedTrack) -> ResolvedTrack {
221
- guard let children = resolvedTrack.children else { return resolvedTrack }
222
- let hydratedChildren = children.map { hydrateFavorite($0) }
223
- return resolvedTrack.copying(children: hydratedChildren)
224
- }
225
-
226
- // MARK: - Track Cache
227
-
228
- /// Cache a track by both url and src for O(1) lookup from either key.
229
- private func cacheTrack(_ track: Track) {
230
- if let url = track.url {
231
- trackCache.set(url, value: track)
232
- }
233
- if let src = track.src {
234
- trackCache.set(src, value: track)
235
- }
236
- }
237
-
238
- private func cacheChildren(_ resolvedTrack: ResolvedTrack) {
239
- resolvedTrack.children?.forEach { cacheTrack($0) }
167
+ guard let sections = resolvedTrack.sections else { return resolvedTrack }
168
+ return resolvedTrack.copying(sections: sections.map { section in
169
+ section.copying(children: section.children.map { hydrateFavorite($0) })
170
+ })
240
171
  }
241
172
 
242
173
  // MARK: - Navigation
243
174
 
244
- /// Main navigation method - updates path and resolves content.
245
- ///
246
- /// Uses a navigation ID to prevent race conditions when multiple navigations
247
- /// overlap. Only the most recent navigation's result is applied.
248
- func navigate(_ path: String) async throws {
249
- // Increment navigation ID and capture for this navigation
175
+ /// Loads `path` as the current page. `bypassCache` reloads it from the network and keeps the
176
+ /// content on screen meanwhile, for refreshing the current path. A newer load supersedes this one, and its
177
+ /// result or failure is dropped: every caller reports a throw, so a slow /a failing after a
178
+ /// fast /b succeeded would otherwise show /a's error over /b's content.
179
+ func load(_ path: String, bypassCache: Bool = false) async throws {
250
180
  currentNavigationId += 1
251
181
  let navigationId = currentNavigationId
252
-
253
182
  self.path = path
254
- content = nil // Clear for loading state
255
-
256
- let resolved = try await resolve(path)
257
-
258
- // Only apply result if this is still the current navigation
259
- guard navigationId == currentNavigationId else { return }
260
-
261
- content = resolved
183
+ if bypassCache {
184
+ // A failed reload must not leave the stale page to be served from the cache.
185
+ contentCache.remove(path)
186
+ } else {
187
+ content = nil
188
+ }
189
+ do {
190
+ let resolved = try await resolve(path, useCache: !bypassCache)
191
+ guard navigationId == currentNavigationId else { return }
192
+ content = resolved
193
+ } catch {
194
+ guard navigationId == currentNavigationId else { return }
195
+ throw error
196
+ }
262
197
  }
263
198
 
264
199
  /// Resolves content for a path with optional caching.
@@ -271,19 +206,27 @@ final class BrowserManager {
271
206
  return hydrateChildren(cached)
272
207
  }
273
208
 
274
- let resolvedTrack = try await resolveUncached(normalizedPath)
209
+ let resolvedTrack = try await pages.resolve(normalizedPath)
275
210
 
276
211
  // Cache the resolved content for future navigation
277
212
  contentCache.set(normalizedPath, value: resolvedTrack)
278
213
 
279
- // Cache children for track lookups
280
- cacheChildren(resolvedTrack)
281
-
282
214
  return hydrateChildren(resolvedTrack)
283
215
  }
284
216
 
285
217
  /// Invalidates the content cache for a specific path.
286
- func invalidateContentCache(_ path: String) {
218
+ ///
219
+ /// Expects a *container* path. A contextual path (`…?__trackId=…`) addresses
220
+ /// a child, not a cache entry — entries are keyed by the stripped parent — so
221
+ /// passing one used to remove nothing at all, silently. Android `require`s the
222
+ /// same precondition; this throws so the misuse surfaces identically on both
223
+ /// platforms instead of no-opping on one.
224
+ func invalidateContentCache(_ path: String) throws {
225
+ guard !BrowserPathHelper.isContextual(path) else {
226
+ throw BrowserError.invalidConfiguration(
227
+ "invalidateContentCache() expects a container path, not a contextual path: \(path)",
228
+ )
229
+ }
287
230
  contentCache.remove(path)
288
231
  }
289
232
 
@@ -291,324 +234,23 @@ final class BrowserManager {
291
234
  /// Runs from `invalidateAllContent`, so re-resolve the layer thunks too.
292
235
  func clearContentCache() {
293
236
  contentCache.clear()
294
- layerGeneration += 1
295
- }
296
-
297
- /// Refreshes the current path by invalidating cache and re-resolving.
298
- ///
299
- /// Uses navigation ID tracking to prevent race conditions.
300
- func refresh() async throws {
301
- // Increment navigation ID and capture for this refresh
302
- currentNavigationId += 1
303
- let navigationId = currentNavigationId
304
-
305
- let currentPath = path
306
- contentCache.remove(currentPath)
307
- let resolved = try await resolve(currentPath, useCache: false)
308
-
309
- // Only apply result if this is still the current navigation
310
- guard navigationId == currentNavigationId else { return }
311
-
312
- content = resolved
313
- }
314
-
315
- private func resolveUncached(_ path: String) async throws -> ResolvedTrack {
316
- // Match an explicit route (or the '*' default). With no match, fall back to
317
- // the implicit default: fetch the path via the request + browse config.
318
- let routeEntry = (config.routes).flatMap { findBestRouteMatch(path: path, routes: $0) }
319
-
320
- let resolvedTrack: ResolvedTrack = if let (entry, routeMatch) = routeEntry {
321
- try await resolveRouteEntry(entry, path: path, params: routeMatch.params)
322
- } else {
323
- // Implicit default — no route config, just request → browse → fetch path.
324
- try await resolveFromConfig(
325
- nil, path: path, params: ["path": path],
326
- )
327
- }
328
-
329
- // Validate and transform children
330
- if let children = resolvedTrack.children {
331
- let transformed = try await transformChildren(
332
- children, parentPath: path, routeEntry: routeEntry?.0,
333
- )
334
- return resolvedTrack.copying(children: transformed)
335
- }
336
-
337
- return resolvedTrack
338
- }
339
-
340
- // MARK: - Route Resolution
341
-
342
- private func findBestRouteMatch(
343
- path: String,
344
- routes: [NativeRouteEntry],
345
- ) -> (NativeRouteEntry, RouteMatch)? {
346
- // Filter out special routes
347
- let browseRoutes = routes.filter {
348
- $0.path != Self.tabsRoutePath &&
349
- $0.path != Self.searchRoutePath &&
350
- $0.path != Self.defaultRoutePath
351
- }
352
-
353
- // Try to find a matching route
354
- if let match = router.findBestMatch(path: path, routes: browseRoutes) {
355
- return match
356
- }
357
-
358
- // Fall back to __default__ if present
359
- if let defaultRoute = routes.first(where: { $0.path == Self.defaultRoutePath }) {
360
- return (defaultRoute, RouteMatch(params: ["path": path]))
361
- }
362
-
363
- return nil
364
- }
365
-
366
- private func resolveRouteEntry(
367
- _ entry: NativeRouteEntry,
368
- path: String,
369
- params: [String: String],
370
- ) async throws -> ResolvedTrack {
371
- // Priority: callback > config > static
372
- if let callback = entry.browseCallback {
373
- let callbackParam = BrowserSourceCallbackParam(path: path, routeParams: params)
374
- // BrowserSourceCallback may return a BrowseResult synchronously or via a
375
- // Promise. Nitro flattens (ResolvedTrack | BrowseError) | Promise<BrowseResult>
376
- // into a 3-arm variant: sync track, sync error, or a Promise resolving to a
377
- // BrowseResult (which is itself a ResolvedTrack | BrowseError variant).
378
- switch try await callback(callbackParam).await() {
379
- case let .first(resolvedTrack):
380
- return resolvedTrack
381
- case let .second(browseError):
382
- throw BrowserError.callbackError(browseError.error)
383
- case let .third(promise):
384
- switch try await promise.await() {
385
- case let .first(resolvedTrack):
386
- return resolvedTrack
387
- case let .second(browseError):
388
- throw BrowserError.callbackError(browseError.error)
389
- }
390
- }
391
- }
392
-
393
- if let browseConfig = entry.browseConfig {
394
- return try await resolveFromConfig(
395
- browseConfig, path: path, params: params,
396
- )
397
- }
398
-
399
- if let staticContent = entry.browseStatic {
400
- return staticContent
401
- }
402
-
403
- throw BrowserError.contentNotFound(path: path)
404
- }
405
-
406
- /// Builds the HTTP request for an API-backed route by layering
407
- /// request → kind → route configs. `initialQuery` seeds request-level query
408
- /// params (e.g. search q/mode/…) onto the base before the route layer, so they
409
- /// survive into `request.query` even when a layer's transform "wins
410
- /// completely" (it is handed only the base, so the route config's own static
411
- /// query would otherwise be dropped). `routeConfig` is nil for the implicit
412
- /// browse default; `kindConfig` is nil for kinds with no per-kind config.
413
- private func buildApiRequest(
414
- kind kindConfig: TransformableRequestConfig?,
415
- _ routeConfig: TransformableRequestConfig?,
416
- path: String,
417
- params: [String: String],
418
- initialQuery: [String: String]? = nil,
419
- ) async throws -> HttpClient.HttpRequest {
420
- try await ensureLayersResolved()
421
-
422
- var merged = RequestConfig(
423
- method: nil, path: path, baseUrl: nil, headers: nil,
424
- query: nil, body: nil, contentType: nil, userAgent: nil,
425
- )
426
- merged = try await applyLayer(resolvedRequestLayer, to: merged, params: params)
427
- if let initialQuery, !initialQuery.isEmpty {
428
- merged = merged.copying(query: mergeDicts(merged.query, initialQuery))
429
- }
430
- merged = try await applyLayer(kindConfig, to: merged, params: params)
431
- merged = try await applyLayer(routeConfig, to: merged, params: params)
432
-
433
- guard let baseUrl = merged.baseUrl else {
434
- throw BrowserError.invalidConfiguration("No URL configured for route")
435
- }
436
- var url = BrowserPathHelper.buildUrl(baseUrl: baseUrl, path: merged.path ?? path)
437
- if let query = merged.query, !query.isEmpty {
438
- url = BrowserPathHelper.appendQuery(query, to: url)
439
- }
440
-
441
- return HttpClient.HttpRequest(
442
- url: url,
443
- method: merged.method?.stringValue ?? "GET",
444
- headers: merged.headers,
445
- body: merged.body,
446
- contentType: merged.contentType ?? HttpClient.defaultContentType,
447
- userAgent: merged.userAgent ?? HttpClient.defaultUserAgent,
237
+ requests.invalidateLayers()
238
+ // The search slot is content too: a locale switch or a re-auth changes
239
+ // titles, src, artwork and availability, and nothing else ever evicts it,
240
+ // so a repeated query returned pre-invalidation results forever.
241
+ lastSearchKey = nil
242
+ lastSearchResults = nil
243
+ }
244
+
245
+ /// `error` as the app's `formatNavigationError` displays it, or its default display when
246
+ /// there is no formatter or it fails.
247
+ func formatted(_ error: NavigationError, path: String) async -> FormattedNavigationError {
248
+ let fallback = error.defaultFormatted()
249
+ guard let formatter = config.formatNavigationError else { return fallback }
250
+ let params = FormatNavigationErrorParams(
251
+ error: error, defaultFormatted: fallback, path: path, kind: NavigationErrorKind(path: path),
448
252
  )
449
- }
450
-
451
- /// Resolves a browse route: a page object (`{title,url,children:[…]}`).
452
- /// The browse kind layer is the cached resolved layer (`resolvedBrowseLayer`),
453
- /// populated by `ensureLayersResolved()` inside `buildApiRequest`.
454
- private func resolveFromConfig(
455
- _ routeConfig: TransformableRequestConfig?,
456
- path: String,
457
- params: [String: String],
458
- ) async throws -> ResolvedTrack {
459
- try await ensureLayersResolved()
460
- let request = try await buildApiRequest(
461
- kind: resolvedBrowseLayer, routeConfig, path: path, params: params,
462
- )
463
-
464
- logger.debug("Resolving content from API")
465
- logger.debug(" path: \(path)")
466
- logger.debug(" url: \(request.url)")
467
-
468
- let result: JsonResolvedTrack = try await httpClient.requestJson(request, as: JsonResolvedTrack.self)
469
- let nitroResult = result.toNitro()
470
-
471
- logger.debug("Resolved: \(nitroResult.title)")
472
- if let children = nitroResult.children {
473
- logger.debug(" children: \(children.count) tracks")
474
- }
475
-
476
- return nitroResult
477
- }
478
-
479
- /// Applies one request-config layer (request / browse / route) onto a base.
480
- /// A transform (async and/or sync) wins completely: it receives the base and its
481
- /// result replaces it. When both are set they run as a pipeline — async first,
482
- /// then sync. Otherwise the layer's static fields merge over the base. `path` is
483
- /// carried from the base (only a transform may change it).
484
- func applyLayer(
485
- _ layer: TransformableRequestConfig?,
486
- to base: RequestConfig,
487
- params: [String: String],
488
- ) async throws -> RequestConfig {
489
- guard let layer else { return base }
490
- // Run-both transform: async first, then sync (each replaces the running config).
491
- // The bridge await depth — the bug-prone part — is centralised in awaitAsync/SyncConfig.
492
- if layer.transform != nil || layer.transformSync != nil {
493
- var result = base
494
- if let transform = layer.transform { result = try await awaitAsyncConfig(transform(result, params)) }
495
- if let transformSync = layer.transformSync { result = try await awaitSyncConfig(transformSync(result, params)) }
496
- return result
497
- }
498
- return RequestConfig(
499
- method: layer.method ?? base.method,
500
- path: base.path,
501
- baseUrl: layer.baseUrl ?? base.baseUrl,
502
- headers: mergeDicts(base.headers, layer.headers),
503
- query: mergeDicts(base.query, layer.query),
504
- body: layer.body ?? base.body,
505
- contentType: layer.contentType ?? base.contentType,
506
- userAgent: layer.userAgent ?? base.userAgent,
507
- )
508
- }
509
-
510
- /// Merges two optional dictionaries, with override values taking precedence.
511
- /// Delegates to the pure, unit-tested `MediaResolveComposer.mergeDicts`.
512
- func mergeDicts(
513
- _ base: [String: String]?,
514
- _ override: [String: String]?,
515
- ) -> [String: String]? {
516
- MediaResolveComposer.mergeDicts(base, override)
517
- }
518
-
519
- // MARK: - Layer Resolvers
520
-
521
- /// Resolve one layer: invoke its resolver if present, else return the static
522
- /// layer config unchanged. The resolver is Promise-only (the TS layer wraps a
523
- /// sync-or-async thunk in Promise.resolve), so its result is the JS promise
524
- /// behind the bridge promise — a double await.
525
- private func resolveLayer(
526
- config staticConfig: TransformableRequestConfig?,
527
- resolver: (() -> Promise<Promise<TransformableRequestConfig>>)?,
528
- ) async throws -> TransformableRequestConfig? {
529
- guard let resolver else { return staticConfig }
530
- return try await resolver().await().await()
531
- }
532
-
533
- /// Ensure request/browse resolvers are resolved for the current generation,
534
- /// caching the results. Re-resolves after a generation bump (config change /
535
- /// invalidateAllContent). Idempotent within a generation.
536
- func ensureLayersResolved() async throws {
537
- guard resolvedLayerGeneration != layerGeneration else { return }
538
- let generation = layerGeneration
539
- let req = try await resolveLayer(config: config.request, resolver: config.requestResolver)
540
- let brw = try await resolveLayer(config: config.browse, resolver: config.browseResolver)
541
- // Only commit if still current (a newer generation may have started during await).
542
- guard generation == layerGeneration else { return }
543
- resolvedRequestLayer = req
544
- resolvedBrowseLayer = brw
545
- resolvedLayerGeneration = generation
546
- }
547
-
548
- // MARK: - Child Transformation
549
-
550
- private func transformChildren(
551
- _ children: [Track],
552
- parentPath: String,
553
- routeEntry: NativeRouteEntry?,
554
- ) async throws -> [Track] {
555
- var transformed: [Track] = []
556
-
557
- for track in children {
558
- // Validate track has stable identifier. A track carrying an imageRow is
559
- // exempt: a url-less row is a pure preview — never selected, navigated
560
- // to, or cached (its items carry their own identity).
561
- if track.url == nil, track.src == nil, track.imageRow?.isEmpty != false {
562
- throw BrowserError.invalidConfiguration(
563
- "Track must have either 'url' or 'src' for stable identification: \(track.title)",
564
- )
565
- }
566
-
567
- var transformedTrack = track
568
-
569
- if track.src != nil, track.url == nil {
570
- let contextualUrl = BrowserPathHelper.build(parentPath: parentPath, trackId: track.src!)
571
- transformedTrack = track.copying(url: contextualUrl)
572
- }
573
-
574
- // Resolve artwork URL at browse-time (no size context)
575
- let artworkConfig = routeEntry?.artwork ?? config.artwork
576
- if let imageSource = await resolveArtworkUrl(track: transformedTrack, perRouteConfig: artworkConfig) {
577
- transformedTrack = transformedTrack.copying(artworkSource: imageSource)
578
- }
579
-
580
- // Resolve artwork for image row items
581
- if let imageRowItems = transformedTrack.imageRow {
582
- var resolvedItems: [ImageRowItem] = []
583
- for item in imageRowItems {
584
- let itemImageSource = await resolveArtworkUrl(track: item.toTrack(), perRouteConfig: artworkConfig)
585
- // Playable items get a contextual URL like any list row, so a
586
- // thumbnail tap expands into its section's queue instead of a
587
- // queue of one (ADR 0006).
588
- let itemUrl = item.url ?? item.src.map {
589
- BrowserPathHelper.build(parentPath: parentPath, trackId: $0)
590
- }
591
- resolvedItems.append(ImageRowItem(
592
- id: item.id,
593
- url: itemUrl,
594
- src: item.src,
595
- artwork: item.artwork,
596
- artworkSource: itemImageSource,
597
- title: item.title,
598
- artist: item.artist,
599
- album: item.album,
600
- albumUrl: item.albumUrl,
601
- live: item.live,
602
- request: item.request,
603
- ))
604
- }
605
- transformedTrack = transformedTrack.copying(imageRow: resolvedItems)
606
- }
607
-
608
- transformed.append(transformedTrack)
609
- }
610
-
611
- return transformed
253
+ return await (try? formatter(params).await()) ?? fallback
612
254
  }
613
255
 
614
256
  // MARK: - Search
@@ -621,71 +263,43 @@ final class BrowserManager {
621
263
  )
622
264
  }
623
265
 
624
- /// Structured search. Emits `q` plus any of `mode`/`genre`/`artist`/`album`/
625
- /// `title`/`playlist` that are set, mirroring Android's `executeSearchApiRequest`
626
- /// and the web stub. Cached by `query`.
266
+ /// The cache identity of a search. Sorted so key order is stable, and
267
+ /// `\u{1}`-delimited so two different param sets can't serialize alike.
268
+ private static func searchCacheKey(_ params: SearchParams) -> String {
269
+ PageResolver.searchQueryParams(params)
270
+ .sorted { $0.key < $1.key }
271
+ .map { "\($0.key)=\($0.value)" }
272
+ .joined(separator: "\u{1}")
273
+ }
274
+
275
+ /// Structured search, cached by the full parameter set.
276
+ ///
277
+ /// Voice intents reuse `query` across different structures —
278
+ /// `MediaIntentCriteria` falls back to the structured value when there is no
279
+ /// spoken name, so "play jazz" (genre) and a text search for "jazz" arrive
280
+ /// with an identical `query` — hence `searchCacheKey` rather than `query`.
627
281
  func search(_ params: SearchParams) async throws -> ResolvedTrack {
628
282
  let query = params.query
283
+ let cacheKey = Self.searchCacheKey(params)
629
284
 
630
- // Check cache - re-hydrate favorites since they may have changed
631
- if query == lastSearchQuery, let results = lastSearchResults {
632
- let hydratedResults = results.map { hydrateFavorite($0) }
633
- return makeSearchResult(query: query, results: hydratedResults)
634
- }
635
-
636
- guard let routes = config.routes else {
637
- throw BrowserError.contentNotFound(path: Self.searchRoutePath)
638
- }
639
-
640
- guard let searchEntry = routes.first(where: { $0.path == Self.searchRoutePath }) else {
641
- throw BrowserError.contentNotFound(path: Self.searchRoutePath)
642
- }
643
-
644
- var results: [Track]
645
-
646
- if let callback = searchEntry.searchCallback {
647
- let outerPromise = callback(params)
648
- let innerPromise = try await outerPromise.await()
649
- results = try await innerPromise.await()
650
- } else if let searchConfig = searchEntry.searchConfig {
651
- // Search is its own kind — request → search route (no browse layer). The
652
- // endpoint returns a bare Track array (unlike browse's page object). The
653
- // search params go through `initialQuery` so they land in `request.query`
654
- // (and the URL); a config with a transform only ever sees the base.
655
- // Seed the search config's static path onto the base — a layer's static
656
- // path never applies (carried from the base; only a transform may change
657
- // it). Matches the web stub's fetchSearchResults and Android.
658
- var queryParams: [String: String] = ["q": query]
659
- if let mode = params.mode { queryParams["mode"] = mode.stringValue }
660
- if params.reference == .my { queryParams["reference"] = "my" }
661
- if let genre = params.genre { queryParams["genre"] = genre }
662
- if let artist = params.artist { queryParams["artist"] = artist }
663
- if let album = params.album { queryParams["album"] = album }
664
- if let title = params.title { queryParams["title"] = title }
665
- if let playlist = params.playlist { queryParams["playlist"] = playlist }
666
-
667
- let request = try await buildApiRequest(
668
- kind: nil, searchConfig, path: searchConfig.path ?? "", params: ["q": query],
669
- initialQuery: queryParams,
670
- )
671
- logger.debug("Searching via API: \(request.url)")
672
- let jsonTracks: [JsonTrack] = try await httpClient.requestJson(request, as: [JsonTrack].self)
673
- results = jsonTracks.map { $0.toNitro() }
674
- } else {
675
- throw BrowserError.contentNotFound(path: Self.searchRoutePath)
285
+ // Check cache - hydrate favorites on the way out, since they may have
286
+ // changed since the search ran (the slot holds the raw results for exactly
287
+ // this reason — see below).
288
+ if cacheKey == lastSearchKey, let results = lastSearchResults {
289
+ return makeSearchResult(query: query, results: results)
676
290
  }
677
291
 
678
- // Hydrate favorites in results
679
- let hydratedResults = results.map { hydrateFavorite($0) }
292
+ let results = try await pages.search(params)
680
293
 
681
- // Cache results
682
- lastSearchQuery = query
683
- lastSearchResults = hydratedResults
294
+ // Cache the RAW results, never the hydrated ones: hydration is one-shot
295
+ // (`hydrateFavorite` skips a track whose `favorited` is already set), so a
296
+ // hydrated slot would serve the favorites as they were at first search
297
+ // forever. The content cache keys on the same rule — it stores the
298
+ // unhydrated resolve and hydrates in `resolve()`.
299
+ lastSearchKey = cacheKey
300
+ lastSearchResults = results
684
301
 
685
- // Cache individual tracks
686
- hydratedResults.forEach { cacheTrack($0) }
687
-
688
- return makeSearchResult(query: query, results: hydratedResults)
302
+ return makeSearchResult(query: query, results: results)
689
303
  }
690
304
 
691
305
  /// Search and return playable tracks for "play «X»" voice intents, drilling
@@ -693,37 +307,44 @@ final class BrowserManager {
693
307
  /// station. Decision in `SearchDrillIn.playable`; mirrors Android's
694
308
  /// `searchPlayable`.
695
309
  func searchPlayable(_ params: SearchParams) async throws -> [Track]? {
696
- let children = try await (search(params).children) ?? []
697
- return try await SearchDrillIn.playable(from: children) { [self] url in
698
- logger.debug("First search result is browsable-only, resolving: \(url)")
699
- return try await (resolve(url).children) ?? []
310
+ // Voice search never matches a disabled track (Track.disabled) — filtered
311
+ // before drill-in so an unavailable first result can't capture the play.
312
+ let children = try await ((search(params).flattenedChildren) ?? [])
313
+ .filter { $0.disabled != true }
314
+ return try await SearchDrillIn.playable(from: children) { [self] path in
315
+ logger.debug("First search result is browsable-only, resolving: \(path)")
316
+ return try await ((resolve(path).flattenedChildren) ?? []).filter { $0.disabled != true }
700
317
  }
701
318
  }
702
319
 
320
+ /// Wraps raw search results as a resolved page, hydrating favorites on the
321
+ /// way out — the single exit for `search`, so an unhydrated search page can't
322
+ /// be emitted (the same funnel `resolve()` gets from `hydrateChildren`).
703
323
  private func makeSearchResult(query: String, results: [Track]) -> ResolvedTrack {
324
+ // Search results are a flat list; as a resolved page they are one
325
+ // untitled section (ADR 0010).
704
326
  ResolvedTrack(
705
- url: BrowserPathHelper.createSearchPath(query),
706
- children: results,
327
+ path: BrowserPathHelper.createSearchPath(query),
328
+ style: nil,
329
+ sections: [.untitled(results.map { hydrateFavorite($0) })],
330
+ children: nil,
707
331
  carPlaySiriListButton: nil,
708
332
  id: nil,
709
333
  src: nil,
710
334
  artwork: nil,
711
335
  artworkSource: nil, request: nil,
712
- artworkCarPlayTinted: nil,
713
336
  title: "Search: \(query)",
714
337
  subtitle: nil,
715
338
  artist: nil,
716
- albumUrl: nil,
339
+ albumPath: nil,
717
340
  album: nil,
718
341
  description: nil,
719
342
  genre: nil,
720
343
  duration: nil,
721
- style: nil,
722
- childrenStyle: nil,
344
+ disabled: nil,
723
345
  favorited: nil,
724
- groupTitle: nil,
725
346
  live: nil,
726
- imageRow: nil,
347
+ icyCharset: nil,
727
348
  )
728
349
  }
729
350
 
@@ -731,55 +352,46 @@ final class BrowserManager {
731
352
 
732
353
  /// Query navigation tabs.
733
354
  func queryTabs() async throws -> [Track] {
734
- guard let routes = config.routes else {
735
- return []
736
- }
737
-
738
- guard let tabsEntry = routes.first(where: { $0.path == Self.tabsRoutePath }) else {
739
- return []
740
- }
741
-
742
- let resolved = try await resolveRouteEntry(tabsEntry, path: Self.tabsRoutePath, params: [:])
743
- let tabTracks = resolved.children ?? []
744
-
745
- tabs = tabTracks
746
- return tabTracks
355
+ guard let tabs = try await pages.tabs() else { return [] }
356
+ self.tabs = tabs
357
+ return tabs
747
358
  }
748
359
 
749
360
  // MARK: - Queue Expansion (for CarPlay/external controllers)
750
361
 
751
- /// Expands a contextual URL to a full queue of playable tracks.
752
- func expandQueueFromContextualUrl(_ url: String) async throws -> (tracks: [Track], selectedIndex: Int)? {
753
- guard BrowserPathHelper.isContextual(url) else { return nil }
362
+ /// Expands a contextual path to a full queue of playable tracks.
363
+ func expandQueueFromContextualPath(_ path: String) async throws -> (tracks: [Track], selectedIndex: Int)? {
364
+ guard BrowserPathHelper.isContextual(path) else { return nil }
754
365
 
755
- let parentPath = BrowserPathHelper.stripTrackId(url)
756
- guard let trackId = BrowserPathHelper.extractTrackId(url) else { return nil }
366
+ let parentPath = BrowserPathHelper.stripTrackId(path)
367
+ guard let trackId = BrowserPathHelper.extractTrackId(path) else { return nil }
757
368
 
758
- logger.debug("Expanding queue from contextual URL")
759
- logger.debug(" url: \(url)")
369
+ logger.debug("Expanding queue from contextual path")
370
+ logger.debug(" path: \(path)")
760
371
  logger.debug(" parentPath: \(parentPath)")
761
372
  logger.debug(" trackId: \(trackId)")
762
373
 
763
374
  // Resolve the parent container
764
375
  let resolved = try await resolve(parentPath, useCache: true)
765
- guard let children = resolved.children else { return nil }
766
-
767
- // Queue scope is the tapped section, not the whole page (ADR 0006). An
768
- // id that no longer appears on the page aborts the expansion — the
769
- // caller falls back to the stored single track (matching Android);
770
- // silently queueing the changed list would resume the wrong station.
771
- let sectionTracks: [Track]
772
- switch SectionScope.section(of: children, containing: trackId) {
773
- case .imageRow(let items):
774
- sectionTracks = items.map { $0.toTrack() }
775
- case .run(let tracks):
776
- sectionTracks = tracks
777
- case nil:
376
+ guard let sections = resolved.normalizedSections else { return nil }
377
+
378
+ // Queue scope is the tapped section, not the whole page (ADR 0006). The
379
+ // stamped flat index pins which section (and which copy) was tapped when
380
+ // the same identity appears more than once; it is only a tie-breaker —
381
+ // a stale index falls back to the first identity match. An id that no
382
+ // longer appears on the page at all aborts the expansion — the caller
383
+ // falls back to the stored single track (matching Android); silently
384
+ // queueing the changed list would resume the wrong station.
385
+ let tappedIndex = BrowserPathHelper.extractIndex(path)
386
+ guard let scoped = SectionScope.scoped(in: sections, containing: trackId, tappedIndex: tappedIndex) else {
778
387
  return nil
779
388
  }
389
+ let sectionTracks = scoped.section.children
390
+ let tappedOffset = scoped.tappedOffset
780
391
 
781
- // Filter to playable tracks (have src)
782
- let playableTracks = sectionTracks.filter { $0.src != nil }
392
+ // Filter to playable tracks (have src). A disabled track is unavailable —
393
+ // queue expansion excludes it, so auto-advance never meets one.
394
+ let playableTracks = sectionTracks.filter { $0.src != nil && $0.disabled != true }
783
395
  guard !playableTracks.isEmpty else { return nil }
784
396
 
785
397
  logger.debug("Found \(playableTracks.count) playable tracks")
@@ -787,10 +399,18 @@ final class BrowserManager {
787
399
  logger.debug(" [\(index)] \(track.title) - src: \(track.src ?? "nil")")
788
400
  }
789
401
 
790
- // Find selected track index
791
- // Unreachable in practice (the section was found BY this id), but a miss
792
- // must abort rather than silently start the queue at the wrong track.
793
- guard let selectedIndex = playableTracks.firstIndex(where: { $0.src == trackId }) else {
402
+ // Find selected track index: the pinned copy when the stamp survived,
403
+ // else the first identity match. A miss must abort rather than silently
404
+ // start the queue at the wrong track (unreachable in practice — the
405
+ // section was found BY this id).
406
+ let selectedIndex: Int
407
+ if let tappedOffset, sectionTracks[tappedOffset].src != nil, sectionTracks[tappedOffset].disabled != true {
408
+ selectedIndex =
409
+ sectionTracks.prefix(through: tappedOffset)
410
+ .count(where: { $0.src != nil && $0.disabled != true }) - 1
411
+ } else if let index = playableTracks.firstIndex(where: { $0.identity == trackId }) {
412
+ selectedIndex = index
413
+ } else {
794
414
  return nil
795
415
  }
796
416
  logger.debug("Selected track index: \(selectedIndex)")
@@ -802,20 +422,6 @@ final class BrowserManager {
802
422
 
803
423
  return (tracks: playableTracks, selectedIndex: selectedIndex)
804
424
  }
805
-
806
- // MARK: - Accessors
807
-
808
- func getPath() -> String {
809
- path
810
- }
811
-
812
- func getContent() -> ResolvedTrack? {
813
- content
814
- }
815
-
816
- func getTabs() -> [Track]? {
817
- tabs
818
- }
819
425
  }
820
426
 
821
427
  // MARK: - TrackSelectionBrowser