react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.20

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