react-native-audio-browser 0.2.0-next.11 → 0.2.0-next.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. package/README.md +14 -4
  2. package/android/build.gradle +20 -1
  3. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +115 -26
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +262 -188
  5. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  6. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
  7. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +95 -58
  8. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
  9. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
  10. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +19 -7
  11. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  12. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  13. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
  14. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
  15. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  16. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +109 -74
  17. package/android/src/main/java/com/audiobrowser/player/Player.kt +51 -30
  18. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  19. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  20. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  21. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  22. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
  23. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +52 -53
  24. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +74 -41
  25. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  26. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +229 -50
  27. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  28. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
  29. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  30. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
  31. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  32. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
  33. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  34. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  35. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  36. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  37. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  38. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  39. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  40. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  41. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  42. package/ios/Browser/BrowserConfig.swift +102 -5
  43. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  44. package/ios/Browser/BrowserManager.swift +229 -180
  45. package/ios/Browser/BrowserPathHelper.swift +44 -44
  46. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  47. package/ios/Browser/JsonModels.swift +258 -106
  48. package/ios/Browser/SearchDrillIn.swift +6 -6
  49. package/ios/Browser/SectionPresentation.swift +64 -0
  50. package/ios/Browser/SectionScope.swift +58 -24
  51. package/ios/Browser/StyleResolver.swift +58 -0
  52. package/ios/Browser/TabBarEntries.swift +9 -3
  53. package/ios/Browser/TrackIdTemplate.swift +37 -0
  54. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  55. package/ios/CarPlay/CarPlayController.swift +227 -85
  56. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  57. package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
  58. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  59. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  60. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  61. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  62. package/ios/Extension/Section+Copying.swift +31 -0
  63. package/ios/Extension/Track+Copying.swift +6 -12
  64. package/ios/Extension/Track+Identity.swift +18 -0
  65. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  66. package/ios/Http/HttpClient.swift +8 -2
  67. package/ios/HybridAudioBrowser.swift +166 -45
  68. package/ios/Model/NitroTypeStubs.swift +106 -21
  69. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  70. package/ios/Player/MediaIntentCriteria.swift +13 -5
  71. package/ios/Player/MediaLoader.swift +33 -9
  72. package/ios/Player/NowPlayingUpdater.swift +54 -6
  73. package/ios/Player/PlaybackCoordinator.swift +13 -6
  74. package/ios/Player/PlaybackTimer.swift +8 -2
  75. package/ios/Player/QueueManager.swift +36 -10
  76. package/ios/Player/RetryManager.swift +4 -3
  77. package/ios/Player/ShuffleOrder.swift +23 -0
  78. package/ios/Player/SleepTimerHandling.swift +13 -0
  79. package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
  80. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  81. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  82. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  83. package/ios/Tests/JsonModelsTests.swift +150 -1
  84. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  85. package/ios/Tests/QueueManagerTests.swift +72 -6
  86. package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
  87. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  88. package/ios/Tests/SearchDrillInTests.swift +16 -16
  89. package/ios/Tests/SectionPresentationTests.swift +90 -0
  90. package/ios/Tests/SectionScopeTests.swift +87 -55
  91. package/ios/Tests/StyleResolverTests.swift +127 -0
  92. package/ios/Tests/Support/TestTypes.swift +7 -0
  93. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  94. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  95. package/ios/Tests/TrackIdentityTests.swift +20 -0
  96. package/ios/Tests/TrackSelectorTests.swift +168 -33
  97. package/ios/TrackPlayer.swift +57 -34
  98. package/ios/TrackSelector.swift +45 -30
  99. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  100. package/ios/Util/NetworkMonitor.swift +43 -26
  101. package/ios/Util/SFSymbolRenderer.swift +24 -2
  102. package/ios/Util/SVGProcessor.swift +9 -2
  103. package/lib/commonjs/AudioBrowser.js +11 -0
  104. package/lib/commonjs/AudioBrowser.js.map +1 -1
  105. package/lib/commonjs/features/browser-config.js +11 -8
  106. package/lib/commonjs/features/browser-config.js.map +1 -1
  107. package/lib/commonjs/features/browser.js +2 -2
  108. package/lib/commonjs/features/favorites.js +13 -10
  109. package/lib/commonjs/features/favorites.js.map +1 -1
  110. package/lib/commonjs/features/playback/controls.js +4 -2
  111. package/lib/commonjs/features/playback/controls.js.map +1 -1
  112. package/lib/commonjs/features/playback/progress.js +2 -1
  113. package/lib/commonjs/features/playback/progress.js.map +1 -1
  114. package/lib/commonjs/features/playback/rate.js +8 -1
  115. package/lib/commonjs/features/playback/rate.js.map +1 -1
  116. package/lib/commonjs/features/playback/state.js +2 -3
  117. package/lib/commonjs/features/playback/state.js.map +1 -1
  118. package/lib/commonjs/features/player/options.js.map +1 -1
  119. package/lib/commonjs/features/player/setup.js +3 -1
  120. package/lib/commonjs/features/player/setup.js.map +1 -1
  121. package/lib/commonjs/features/player/validateOptions.js +2 -0
  122. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  123. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  124. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  125. package/lib/commonjs/features/queue/queue.js +11 -4
  126. package/lib/commonjs/features/queue/queue.js.map +1 -1
  127. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  128. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  129. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  130. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  131. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  132. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  133. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  134. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  135. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  136. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  137. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  138. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  139. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  140. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  141. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  142. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  143. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  144. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  145. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  146. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  147. package/lib/commonjs/web/browser/sections.js +89 -0
  148. package/lib/commonjs/web/browser/sections.js.map +1 -0
  149. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  150. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  151. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  152. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  153. package/lib/module/AudioBrowser.js +1 -0
  154. package/lib/module/AudioBrowser.js.map +1 -1
  155. package/lib/module/features/browser-config.js +11 -8
  156. package/lib/module/features/browser-config.js.map +1 -1
  157. package/lib/module/features/browser.js +2 -2
  158. package/lib/module/features/favorites.js +13 -10
  159. package/lib/module/features/favorites.js.map +1 -1
  160. package/lib/module/features/playback/controls.js +4 -2
  161. package/lib/module/features/playback/controls.js.map +1 -1
  162. package/lib/module/features/playback/progress.js +2 -1
  163. package/lib/module/features/playback/progress.js.map +1 -1
  164. package/lib/module/features/playback/rate.js +8 -1
  165. package/lib/module/features/playback/rate.js.map +1 -1
  166. package/lib/module/features/playback/state.js +2 -3
  167. package/lib/module/features/playback/state.js.map +1 -1
  168. package/lib/module/features/player/options.js.map +1 -1
  169. package/lib/module/features/player/setup.js +3 -1
  170. package/lib/module/features/player/setup.js.map +1 -1
  171. package/lib/module/features/player/validateOptions.js +2 -0
  172. package/lib/module/features/player/validateOptions.js.map +1 -1
  173. package/lib/module/features/queue/activeTrack.js +14 -3
  174. package/lib/module/features/queue/activeTrack.js.map +1 -1
  175. package/lib/module/features/queue/queue.js +11 -4
  176. package/lib/module/features/queue/queue.js.map +1 -1
  177. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  178. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  179. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  180. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  181. package/lib/module/utils/getTrackIdentity.js +20 -0
  182. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  183. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  184. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  185. package/lib/module/web/NativeAudioBrowser.js +32 -24
  186. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  187. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  188. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  189. package/lib/module/web/browser/BrowserManager.js +144 -87
  190. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  191. package/lib/module/web/browser/FavoriteManager.js +31 -21
  192. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  193. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  194. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  195. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  196. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  197. package/lib/module/web/browser/sections.js +83 -0
  198. package/lib/module/web/browser/sections.js.map +1 -0
  199. package/lib/module/web/player/NowPlayingManager.js +4 -3
  200. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  201. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  202. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  203. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  204. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  205. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  206. package/lib/typescript/src/features/browser.d.ts +2 -2
  207. package/lib/typescript/src/features/favorites.d.ts +13 -10
  208. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  209. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  210. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  211. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  212. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  213. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  214. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  215. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  216. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  217. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  218. package/lib/typescript/src/features/player/options.d.ts +21 -19
  219. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  220. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  221. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  222. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  223. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  224. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  225. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  226. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  227. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  228. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  229. package/lib/typescript/src/types/browser-nodes.d.ts +361 -126
  230. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  231. package/lib/typescript/src/types/browser.d.ts +33 -53
  232. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  233. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  234. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  235. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  236. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  237. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  238. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  239. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  240. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  241. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  242. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  243. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
  244. package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
  245. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  246. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  247. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
  248. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
  249. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
  250. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
  251. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
  252. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
  253. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
  254. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
  255. 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
  256. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
  257. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  258. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
  259. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
  260. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
  261. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  262. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +33 -14
  263. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  264. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  265. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
  266. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +22 -10
  267. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
  268. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +10 -7
  269. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  270. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  271. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  272. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
  273. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  274. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +55 -51
  275. package/nitrogen/generated/android/c++/JSection.hpp +120 -0
  276. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  277. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  278. package/nitrogen/generated/android/c++/JTrack.hpp +23 -48
  279. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
  280. package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
  281. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
  282. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  283. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  284. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  285. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +18 -6
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
  297. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +158 -82
  298. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +25 -10
  299. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +25 -10
  300. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  301. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  302. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  303. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  304. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  305. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  306. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  307. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  308. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  309. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  310. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  311. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  312. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  313. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  314. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  315. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  316. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  317. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  318. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  319. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  320. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  321. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  322. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  323. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  324. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  325. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  326. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  327. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  328. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  329. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  330. package/package.json +7 -2
  331. package/src/AudioBrowser.ts +1 -0
  332. package/src/features/browser-config.test.ts +21 -6
  333. package/src/features/browser-config.ts +11 -8
  334. package/src/features/browser.test.ts +1 -1
  335. package/src/features/browser.ts +2 -2
  336. package/src/features/favorites.ts +13 -10
  337. package/src/features/hook-wiring.test.ts +63 -15
  338. package/src/features/playback/controls.ts +4 -2
  339. package/src/features/playback/progress.ts +2 -1
  340. package/src/features/playback/rate.ts +8 -1
  341. package/src/features/playback/state.ts +2 -3
  342. package/src/features/playback/trackPlaybackTime.ts +1 -1
  343. package/src/features/player/options.ts +27 -19
  344. package/src/features/player/setup.test.ts +12 -3
  345. package/src/features/player/setup.ts +3 -1
  346. package/src/features/player/validateOptions.ts +2 -0
  347. package/src/features/queue/activeTrack.ts +14 -3
  348. package/src/features/queue/queue.ts +11 -4
  349. package/src/nitro-stub-drift.test.ts +40 -0
  350. package/src/public-surface.test.ts +40 -0
  351. package/src/specs/audio-browser.nitro.ts +4 -3
  352. package/src/style-resolution.test.ts +210 -0
  353. package/src/types/browser-native.ts +2 -2
  354. package/src/types/browser-nodes.ts +377 -135
  355. package/src/types/browser.ts +34 -55
  356. package/src/utils/LazyNativeEmitter.ts +20 -10
  357. package/src/utils/NativeUpdatedValue.ts +29 -13
  358. package/src/utils/getTrackIdentity.ts +20 -0
  359. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  360. package/src/web/NativeAudioBrowser.ts +37 -22
  361. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  362. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  363. package/src/web/browser/BrowserManager.ts +176 -95
  364. package/src/web/browser/FavoriteManager.test.ts +84 -0
  365. package/src/web/browser/FavoriteManager.ts +28 -23
  366. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  367. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  368. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  369. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  370. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  371. package/src/web/browser/sections.test.ts +91 -0
  372. package/src/web/browser/sections.ts +72 -0
  373. package/src/web/player/NowPlayingManager.ts +4 -3
  374. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  375. package/src/web/util/BrowserPathHelper.ts +61 -13
  376. package/website/guide/android-auto.md +114 -0
  377. package/website/guide/android-certificates.md +409 -0
  378. package/website/guide/artwork.md +255 -0
  379. package/website/guide/audio-output.md +135 -0
  380. package/website/guide/automotive.md +86 -0
  381. package/website/guide/basic-usage.md +151 -0
  382. package/website/guide/battery.md +115 -0
  383. package/website/guide/browser.md +581 -0
  384. package/website/guide/carplay.md +620 -0
  385. package/website/guide/configuration.md +173 -0
  386. package/website/guide/equalizer.md +231 -0
  387. package/website/guide/errors.md +310 -0
  388. package/website/guide/favorites.md +154 -0
  389. package/website/guide/gate.md +143 -0
  390. package/website/guide/getting-started.md +46 -0
  391. package/website/guide/hooks.md +238 -0
  392. package/website/guide/live-streams.md +125 -0
  393. package/website/guide/metadata.md +239 -0
  394. package/website/guide/migrating-from-track-player.md +244 -0
  395. package/website/guide/native-callback-fetch.md +127 -0
  396. package/website/guide/network.md +86 -0
  397. package/website/guide/now-playing.md +175 -0
  398. package/website/guide/playback.md +328 -0
  399. package/website/guide/queue.md +300 -0
  400. package/website/guide/remote-controls.md +256 -0
  401. package/website/guide/search.md +255 -0
  402. package/website/guide/sleep-timer.md +268 -0
  403. package/website/guide/track.md +246 -0
  404. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  405. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  406. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  407. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  408. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  409. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  410. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  411. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  412. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  413. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  414. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  415. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  416. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -1,5 +1,6 @@
1
1
  package com.audiobrowser.player
2
2
 
3
+ import com.audiobrowser.extension.identity
3
4
  import com.audiobrowser.util.url
4
5
  import com.margelo.nitro.audiobrowser.FormatNowPlayingParams
5
6
  import com.margelo.nitro.audiobrowser.NowPlayingMetadata
@@ -105,10 +106,11 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
105
106
  private var lastPublished: Published? = null
106
107
 
107
108
  /**
108
- * The track id whose now-playing artwork has been resolved (or is being resolved), so a
109
- * `nowPlayingArtwork` resolve runs once per track instead of on every render.
109
+ * The track identity (id-or-src, falling back to path — same key as [Published]) whose
110
+ * now-playing artwork has been resolved (or is being resolved), so a `nowPlayingArtwork` resolve
111
+ * runs once per track — id-less tracks included — instead of on every render.
110
112
  */
111
- private var artworkResolvedForTrackId: String? = null
113
+ private var artworkResolvedForTrack: String? = null
112
114
 
113
115
  /** Size hint (px, square) for now-playing artwork. Mirrors iOS (screen-width-capped to 1200). */
114
116
  private val artworkSizePx = 1200.0
@@ -164,7 +166,7 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
164
166
  duration = track.duration,
165
167
  artwork = track.artwork?.url,
166
168
  description = track.description,
167
- mediaId = track.src ?: track.url,
169
+ mediaId = track.src ?: track.path,
168
170
  genre = track.genre,
169
171
  )
170
172
  }
@@ -227,7 +229,7 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
227
229
  // default on null/throw.
228
230
  val activeFormatter = if (enabled) formatter else null
229
231
  if (activeFormatter != null) {
230
- val capturedId = track.src ?: track.url
232
+ val capturedId = track.src ?: track.path
231
233
  // Gate the raw load-control signal to the buffering state so `stalled` is correct on its
232
234
  // own: ExoPlayer's rebuffering flag is polled on a different cadence than state transitions
233
235
  // and can linger true into the PLAYING transition as a rebuffer recovers. Classify by
@@ -266,7 +268,7 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
266
268
  formatted != null &&
267
269
  current != null &&
268
270
  currentIdx != null &&
269
- (current.src ?: current.url) == capturedId &&
271
+ (current.src ?: current.path) == capturedId &&
270
272
  renderGeneration == generation
271
273
  ) {
272
274
  applyFields(
@@ -293,7 +295,7 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
293
295
  // the same fields are recomputed often; an unconditional stamp would churn the MediaSession
294
296
  // (and flicker Android Auto / now-playing) for no visible change. Keyed on the track identity
295
297
  // so a new track with a coincidentally identical line still publishes.
296
- val published = Published(index, track.src ?: track.url, title, secondaryLine, album)
298
+ val published = Published(index, track.identity ?: track.path, title, secondaryLine, album)
297
299
  if (published == lastPublished) return
298
300
  lastPublished = published
299
301
 
@@ -307,28 +309,23 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
307
309
 
308
310
  /**
309
311
  * Resolves the playing track's now-playing artwork and stamps it onto the playing media item.
310
- * Guarded exactly like iOS: only when `nowPlayingArtwork` is configured AND the track has a
311
- * non-empty id (so the `{id}` token never resolves to an empty string). Otherwise the existing
312
- * artworkUri — which came from `artwork` (browse list path) — is left in place.
312
+ * Runs for EVERY playing track when `nowPlayingArtwork` is configured — id or not, exactly like
313
+ * iOS (an id-less track leaves any `{id}` token unfilled; the resolver warns and the request
314
+ * visibly 404s). Without a config, the existing artworkUri — which came from `artwork` (browse
315
+ * list path) — is left in place.
313
316
  */
314
317
  private fun maybeResolveArtwork(track: Track) {
315
- val trackId =
316
- track.id?.takeIf { it.isNotEmpty() }
317
- ?: run {
318
- // No id → skip nowPlayingArtwork entirely; keep the existing (browse) artworkUri.
319
- artworkResolvedForTrackId = null
320
- return
321
- }
322
-
323
318
  if (!surface.hasNowPlayingArtworkConfig) {
324
319
  // No now-playing artwork config (or no browser) → fall back to the existing artworkUri.
325
- artworkResolvedForTrackId = null
320
+ artworkResolvedForTrack = null
326
321
  return
327
322
  }
328
323
 
329
- // Already resolved (or resolving) for this track id — avoid a redundant resolve per render.
330
- if (artworkResolvedForTrackId == trackId) return
331
- artworkResolvedForTrackId = trackId
324
+ // Already resolved (or resolving) for this track — avoid a redundant resolve per render.
325
+ // Keyed on track identity (id-or-src, falling back to path) so id-less tracks dedupe too.
326
+ val trackKey = track.identity ?: track.path
327
+ if (trackKey != null && artworkResolvedForTrack == trackKey) return
328
+ artworkResolvedForTrack = trackKey
332
329
 
333
330
  scope.launch {
334
331
  val uri =
@@ -336,10 +333,10 @@ class NowPlayingUpdater(private val surface: NowPlayingSurface, private val scop
336
333
  ?: return@launch
337
334
 
338
335
  // Apply only if still the same track (a fast skip must not be overwritten by a stale
339
- // result).
336
+ // result), compared on the same identity key the resolve was launched under.
340
337
  val currentIdx = surface.currentIndex ?: return@launch
341
338
  val current = surface.currentTrack ?: return@launch
342
- if (current.id != trackId) return@launch
339
+ if ((current.identity ?: current.path) != trackKey) return@launch
343
340
 
344
341
  surface.stampArtwork(currentIdx, current, uri)
345
342
  }
@@ -4,7 +4,13 @@ import android.content.Context
4
4
  import android.content.SharedPreferences
5
5
  import androidx.core.content.edit
6
6
  import androidx.media3.common.C
7
+ import com.audiobrowser.util.EMPTY_TRACK_STYLE
7
8
  import com.audiobrowser.util.artworkOf
9
+ import com.audiobrowser.util.toArtworkRendering
10
+ import com.audiobrowser.util.toCardImage
11
+ import com.audiobrowser.util.toImageShape
12
+ import com.audiobrowser.util.toStyleDisplay
13
+ import com.audiobrowser.util.toWireString
8
14
  import com.audiobrowser.util.url
9
15
  import com.margelo.nitro.audiobrowser.PlaybackState
10
16
  import com.margelo.nitro.audiobrowser.RepeatMode
@@ -18,12 +24,12 @@ import org.json.JSONObject
18
24
  import timber.log.Timber
19
25
 
20
26
  /**
21
- * Persists playback state for resumption after app restart. Stores the current track URL, position,
22
- * and player settings to enable playback resumption via MediaButtonReceiver (Bluetooth play button,
23
- * etc.).
27
+ * Persists playback state for resumption after app restart. Stores the current track (including its
28
+ * path), position, and player settings to enable playback resumption via MediaButtonReceiver
29
+ * (Bluetooth play button, etc.).
24
30
  *
25
31
  * Settings (repeatMode, shuffleEnabled, playbackSpeed) are auto-persisted when set via properties.
26
- * Position/URL are persisted via save() on pause or track change.
32
+ * Position/track are persisted via save() on pause or track change.
27
33
  */
28
34
  class PlaybackStateStore(private val player: Player) {
29
35
  private val prefs: SharedPreferences =
@@ -32,7 +38,7 @@ class PlaybackStateStore(private val player: Player) {
32
38
  /**
33
39
  * Persisted playback state for resumption.
34
40
  *
35
- * @param track The track being played (includes url for queue expansion)
41
+ * @param track The track being played (includes path for queue expansion)
36
42
  * @param positionMs The playback position in milliseconds (C.TIME_UNSET for default/live edge)
37
43
  * @param repeatMode The repeat mode setting
38
44
  * @param shuffleEnabled Whether shuffle mode was enabled
@@ -148,78 +154,11 @@ class PlaybackStateStore(private val player: Player) {
148
154
  Timber.d("Saved position: positionMs=0 (queue ended)")
149
155
  }
150
156
 
151
- private fun trackToJson(track: Track): String =
152
- JSONObject()
153
- .apply {
154
- put("id", track.id)
155
- put("url", track.url)
156
- put("src", track.src)
157
- put("title", track.title)
158
- put("subtitle", track.subtitle)
159
- put("artist", track.artist)
160
- put("albumUrl", track.albumUrl)
161
- put("album", track.album)
162
- put("artwork", track.artwork?.url)
163
- put("description", track.description)
164
- put("genre", track.genre)
165
- put("duration", track.duration)
166
- put("style", track.style?.name)
167
- put("childrenStyle", track.childrenStyle?.name)
168
- put("favorited", track.favorited)
169
- put("groupTitle", track.groupTitle)
170
- put("live", track.live)
171
- }
172
- .toString()
173
-
174
- private fun trackFromJson(json: String): Track? =
175
- runCatching {
176
- val obj = JSONObject(json)
177
- Track(
178
- id = obj.optString("id").takeIf { it.isNotEmpty() },
179
- url = obj.optString("url").takeIf { it.isNotEmpty() },
180
- src = obj.optString("src").takeIf { it.isNotEmpty() },
181
- title = obj.getString("title"),
182
- subtitle = obj.optString("subtitle").takeIf { it.isNotEmpty() },
183
- artist = obj.optString("artist").takeIf { it.isNotEmpty() },
184
- albumUrl = obj.optString("albumUrl").takeIf { it.isNotEmpty() },
185
- album = obj.optString("album").takeIf { it.isNotEmpty() },
186
- artwork = artworkOf(obj.optString("artwork").takeIf { it.isNotEmpty() }),
187
- artworkSource = null, // Not persisted - will be re-transformed on browse
188
- artworkCarPlayTinted = null, // iOS-only, not persisted
189
- description = obj.optString("description").takeIf { it.isNotEmpty() },
190
- genre = obj.optString("genre").takeIf { it.isNotEmpty() },
191
- duration =
192
- if (obj.has("duration") && !obj.isNull("duration")) obj.getDouble("duration") else null,
193
- style =
194
- obj
195
- .optString("style")
196
- .takeIf { it.isNotEmpty() }
197
- ?.let { runCatching { TrackStyle.valueOf(it) }.getOrNull() },
198
- childrenStyle =
199
- obj
200
- .optString("childrenStyle")
201
- .takeIf { it.isNotEmpty() }
202
- ?.let { runCatching { TrackStyle.valueOf(it) }.getOrNull() },
203
- favorited =
204
- if (obj.has("favorited") && !obj.isNull("favorited")) obj.getBoolean("favorited")
205
- else null,
206
- groupTitle = obj.optString("groupTitle").takeIf { it.isNotEmpty() },
207
- live = if (obj.has("live") && !obj.isNull("live")) obj.getBoolean("live") else null,
208
- // Not persisted: on resumption the contextual URL is re-browsed
209
- // (expandQueueFromContextualUrl), which re-parses each track's request
210
- // from the API's current response and re-caches it.
211
- request = null,
212
- imageRow = null, // Not persisted
213
- )
214
- }
215
- .onFailure { e -> Timber.w(e, "Failed to parse persisted track JSON") }
216
- .getOrNull()
217
-
218
157
  /**
219
158
  * Restores player settings from persisted state and returns the state for queue setup.
220
159
  *
221
160
  * Applies repeatMode, shuffleMode, and playbackSpeed to the player. The caller is responsible for
222
- * using the returned url and positionMs to set up the playback queue.
161
+ * using the returned path and positionMs to set up the playback queue.
223
162
  *
224
163
  * @return PersistedState if available, null otherwise
225
164
  */
@@ -228,7 +167,11 @@ class PlaybackStateStore(private val player: Player) {
228
167
  // Apply player settings from persisted state
229
168
  player.repeatMode = state.repeatMode
230
169
  player.shuffleMode = state.shuffleEnabled
231
- player.playbackSpeed = state.playbackSpeed
170
+ // Guarded because this input is untrusted: ExoPlayer's setPlaybackSpeed rejects a
171
+ // non-positive speed with IllegalArgumentException, and a snapshot holding one (a
172
+ // corrupted prefs file — the normal write path can't produce it) would then throw on
173
+ // every resume, permanently, since the bad value stays persisted.
174
+ if (state.playbackSpeed > 0f) player.playbackSpeed = state.playbackSpeed
232
175
 
233
176
  Timber.d(
234
177
  "Restored player settings: repeatMode=${state.repeatMode}, shuffle=${state.shuffleEnabled}, speed=${state.playbackSpeed}"
@@ -255,6 +198,98 @@ class PlaybackStateStore(private val player: Player) {
255
198
  }
256
199
 
257
200
  companion object {
201
+ // Internal (not private) so the round-trip is testable without a Player.
202
+ internal fun trackToJson(track: Track): String =
203
+ JSONObject()
204
+ .apply {
205
+ put("id", track.id)
206
+ put("path", track.path)
207
+ put("src", track.src)
208
+ put("title", track.title)
209
+ put("subtitle", track.subtitle)
210
+ put("artist", track.artist)
211
+ put("albumPath", track.albumPath)
212
+ put("album", track.album)
213
+ put("artwork", track.artwork?.url)
214
+ put("description", track.description)
215
+ put("genre", track.genre)
216
+ put("duration", track.duration)
217
+ // The style block persists as its wire shape (nested object, wire
218
+ // strings) and `disabled` beside it: a restored queue keeps its
219
+ // visible fidelity across process death (ADR 0011).
220
+ put(
221
+ "style",
222
+ track.style?.let { style ->
223
+ JSONObject().apply {
224
+ put("display", style.display?.toWireString())
225
+ put("artworkRendering", style.artworkRendering?.toWireString())
226
+ put("imageShape", style.imageShape?.toWireString())
227
+ put("accessorySymbol", style.accessorySymbol)
228
+ put("cardTint", style.cardTint)
229
+ put("cardImage", style.cardImage?.toWireString())
230
+ }
231
+ },
232
+ )
233
+ put("disabled", track.disabled)
234
+ put("favorited", track.favorited)
235
+ put("live", track.live)
236
+ }
237
+ .toString()
238
+
239
+ internal fun trackFromJson(json: String): Track? =
240
+ runCatching {
241
+ val obj = JSONObject(json)
242
+ Track(
243
+ id = obj.optString("id").takeIf { it.isNotEmpty() },
244
+ path = obj.optString("path").takeIf { it.isNotEmpty() },
245
+ src = obj.optString("src").takeIf { it.isNotEmpty() },
246
+ title = obj.getString("title"),
247
+ subtitle = obj.optString("subtitle").takeIf { it.isNotEmpty() },
248
+ artist = obj.optString("artist").takeIf { it.isNotEmpty() },
249
+ albumPath = obj.optString("albumPath").takeIf { it.isNotEmpty() },
250
+ album = obj.optString("album").takeIf { it.isNotEmpty() },
251
+ artwork = artworkOf(obj.optString("artwork").takeIf { it.isNotEmpty() }),
252
+ artworkSource = null, // Not persisted - will be re-transformed on browse
253
+ description = obj.optString("description").takeIf { it.isNotEmpty() },
254
+ genre = obj.optString("genre").takeIf { it.isNotEmpty() },
255
+ duration =
256
+ if (obj.has("duration") && !obj.isNull("duration")) obj.getDouble("duration")
257
+ else null,
258
+ // Tolerant (ADR 0011): a snapshot written before the block model —
259
+ // `style` as an enum-name string — is not an object, so optJSONObject
260
+ // yields null and the track restores without a declaration.
261
+ style =
262
+ obj
263
+ .optJSONObject("style")
264
+ ?.let { style ->
265
+ TrackStyle(
266
+ display = style.optString("display").toStyleDisplay(),
267
+ artworkRendering = style.optString("artworkRendering").toArtworkRendering(),
268
+ imageShape = style.optString("imageShape").toImageShape(),
269
+ accessorySymbol = style.optString("accessorySymbol").takeIf { it.isNotEmpty() },
270
+ cardTint = style.optString("cardTint").takeIf { it.isNotEmpty() },
271
+ cardImage = style.optString("cardImage").toCardImage(),
272
+ )
273
+ }
274
+ // An empty block collapses to "no declaration" — one shape on
275
+ // every platform, matching the browse decoders.
276
+ ?.takeIf { it != EMPTY_TRACK_STYLE },
277
+ disabled =
278
+ if (obj.has("disabled") && !obj.isNull("disabled")) obj.getBoolean("disabled")
279
+ else null,
280
+ favorited =
281
+ if (obj.has("favorited") && !obj.isNull("favorited")) obj.getBoolean("favorited")
282
+ else null,
283
+ live = if (obj.has("live") && !obj.isNull("live")) obj.getBoolean("live") else null,
284
+ // Not persisted: on resumption the contextual path is re-browsed
285
+ // (expandQueueFromContextualPath), which re-parses each track's request
286
+ // from the API's current response and re-caches it.
287
+ request = null,
288
+ )
289
+ }
290
+ .onFailure { e -> Timber.w(e, "Failed to parse persisted track JSON") }
291
+ .getOrNull()
292
+
258
293
  private const val PREFS_NAME = "audio_browser_playback_state_v3"
259
294
  private const val KEY_POSITION_MS = "position_ms"
260
295
  private const val KEY_REPEAT_MODE = "repeat_mode"
@@ -20,6 +20,7 @@ import com.audiobrowser.browser.displayArtworkSource
20
20
  import com.audiobrowser.browser.resolveArtworkUrl
21
21
  import com.audiobrowser.browser.unattributedArtworkSource
22
22
  import com.audiobrowser.extension.NumberExt.Companion.toSeconds
23
+ import com.audiobrowser.extension.identity
23
24
  import com.audiobrowser.model.PlayerSetupOptions
24
25
  import com.audiobrowser.model.PlayerUpdateOptions
25
26
  import com.audiobrowser.util.CoilBitmapLoader
@@ -36,7 +37,6 @@ import com.margelo.nitro.audiobrowser.ImageSource
36
37
  import com.margelo.nitro.audiobrowser.NativeGateRequest
37
38
  import com.margelo.nitro.audiobrowser.NowPlayingMetadata
38
39
  import com.margelo.nitro.audiobrowser.Playback
39
- import com.margelo.nitro.audiobrowser.PlaybackActiveTrackChangedEvent
40
40
  import com.margelo.nitro.audiobrowser.PlaybackError
41
41
  import com.margelo.nitro.audiobrowser.PlaybackPlayWhenReadyChangedEvent
42
42
  import com.margelo.nitro.audiobrowser.PlaybackProgressUpdatedEvent
@@ -351,7 +351,7 @@ class Player(internal val context: Context) {
351
351
  get() = exoPlayer.nextMediaItemIndex == C.INDEX_UNSET
352
352
 
353
353
  /**
354
- * The source path from which the current queue was expanded (e.g., from a contextual URL). Used
354
+ * The source path from which the current queue was expanded (e.g., from a contextual path). Used
355
355
  * to avoid re-expanding the queue when selecting tracks from the same source.
356
356
  */
357
357
  var queueSourcePath: String? = null
@@ -657,7 +657,7 @@ class Player(internal val context: Context) {
657
657
  * @param tracks The tracks to set as the new queue.
658
658
  * @param startIndex The index to start playback from (default: 0).
659
659
  * @param startPositionMs The position in milliseconds to start from (default: 0).
660
- * @param sourcePath Optional path from which this queue was expanded (for contextual URL
660
+ * @param sourcePath Optional path from which this queue was expanded (for contextual path
661
661
  * optimization).
662
662
  */
663
663
  fun setQueue(
@@ -668,6 +668,9 @@ class Player(internal val context: Context) {
668
668
  ) {
669
669
  val mediaItems = TrackFactory.toMedia3(tracks).toMutableList()
670
670
  exoPlayer.setMediaItems(mediaItems, startIndex, startPositionMs)
671
+ if (mediaItems.isNotEmpty()) {
672
+ exoPlayer.setShuffleOrder(shuffleOrderLedBy(startIndex, mediaItems.size))
673
+ }
671
674
  queueSourcePath = sourcePath
672
675
  exoPlayer.prepare()
673
676
  }
@@ -685,17 +688,27 @@ class Player(internal val context: Context) {
685
688
  /**
686
689
  * Sets the favorited state of the currently playing track. Updates the heart icon in media
687
690
  * controllers without interrupting playback.
691
+ *
692
+ * @return whether the gesture applied — false only when there is no current track (nothing to
693
+ * favorite), so MediaSession callers can report an invalid state instead of a success the
694
+ * controller then watches revert.
688
695
  */
689
- fun setActiveTrackFavorited(favorited: Boolean) {
690
- val currentTrack = this.currentTrack ?: return
696
+ fun setActiveTrackFavorited(favorited: Boolean): Boolean {
697
+ val currentTrack = this.currentTrack ?: return false
691
698
 
692
699
  // Persist to the native favorites cache regardless of player state — the
693
700
  // user's gesture is durable even when there's no active media item to
694
- // update (queue tear-down, between-track gaps, etc.).
695
- currentTrack.src?.let { src -> browser?.browserManager?.updateFavorite(src, favorited) }
701
+ // update (queue tear-down, between-track gaps, etc.). Keyed by the track
702
+ // identity (id when non-blank, else src).
703
+ currentTrack.identity?.let { identity ->
704
+ browser?.browserManager?.updateFavorite(identity, favorited)
705
+ }
696
706
 
707
+ // Cache written above: the gesture is durably recorded even without a media
708
+ // item to restamp (queue tear-down, between-track gaps) — that counts as
709
+ // applied; there is also no visible heart to revert in that state.
697
710
  val index = exoPlayer.currentMediaItemIndex
698
- if (index == C.INDEX_UNSET) return
711
+ if (index == C.INDEX_UNSET) return true
699
712
 
700
713
  // Create updated Track with new favorited state
701
714
  val updatedTrack = currentTrack.copy(favorited = favorited)
@@ -719,28 +732,23 @@ class Player(internal val context: Context) {
719
732
  // Update the heart button icon in notification/Android Auto
720
733
  updateFavoriteButtonState(favorited)
721
734
 
722
- // Notify JS of the favorite state change
735
+ // Notify JS of the favorite state change. This is the ONLY emit: a favorite
736
+ // toggle is an in-place mutation of the active track, not a transition —
737
+ // onPlaybackActiveTrackChanged / onPlaybackQueueChanged stay
738
+ // transition-only, and the useActiveTrack / useQueue hooks subscribe to
739
+ // onFavoriteChanged themselves.
723
740
  callbacks?.onFavoriteChanged(FavoriteChangedEvent(updatedTrack, favorited))
724
-
725
- // Emit active track changed so useActiveTrack() hook updates
726
- val activeTrackEvent =
727
- PlaybackActiveTrackChangedEvent(
728
- lastIndex = index.toDouble(),
729
- lastTrack = currentTrack,
730
- lastPosition = exoPlayer.currentPosition.toSeconds(),
731
- index = index.toDouble(),
732
- track = updatedTrack,
733
- )
734
- callbacks?.onPlaybackActiveTrackChanged(activeTrackEvent)
735
-
736
- // Emit queue changed so useQueue() hook updates
737
- callbacks?.onPlaybackQueueChanged(tracks)
741
+ return true
738
742
  }
739
743
 
740
- /** Toggles the favorited state of the currently playing track. */
741
- fun toggleActiveTrackFavorited() {
742
- val currentTrack = this.currentTrack ?: return
743
- setActiveTrackFavorited(currentTrack.favorited != true)
744
+ /**
745
+ * Toggles the favorited state of the currently playing track.
746
+ *
747
+ * @return whether the toggle applied — false when there is no current track.
748
+ */
749
+ fun toggleActiveTrackFavorited(): Boolean {
750
+ val currentTrack = this.currentTrack ?: return false
751
+ return setActiveTrackFavorited(currentTrack.favorited != true)
744
752
  }
745
753
 
746
754
  // MARK: - Now Playing Metadata
@@ -926,10 +934,23 @@ class Player(internal val context: Context) {
926
934
  }
927
935
  }
928
936
 
929
- /** Jump to the live edge (default position) of a live item; no-op otherwise. */
937
+ /**
938
+ * Jump to the live edge of the current track; no-op unless it declares `live` — the track's own
939
+ * declaration is the gate (matching iOS and the spec), not ExoPlayer's stream-derived liveness,
940
+ * which never marks progressive ICY radio live. A stream with a live window seeks to its edge;
941
+ * anything else reconnects — for a non-seekable stream a fresh connection IS the live edge. A
942
+ * stale URL recovers through the failure-armed re-resolve in [TransformingDataSource].
943
+ */
930
944
  fun seekToLiveEdge() {
945
+ if (currentTrack?.live != true) return
931
946
  if (exoPlayer.isCurrentMediaItemLive) {
932
947
  exoPlayer.seekToDefaultPosition()
948
+ } else {
949
+ // ExoPlayer.prepare() only re-prepares from STATE_IDLE, hence the raw stop() — not
950
+ // Player.stop(), which would drop the play intent this reconnect must preserve.
951
+ exoPlayer.stop()
952
+ exoPlayer.seekToDefaultPosition()
953
+ exoPlayer.prepare()
933
954
  }
934
955
  }
935
956
 
@@ -1249,8 +1270,8 @@ class Player(internal val context: Context) {
1249
1270
 
1250
1271
  if (capabilitiesChanged) {
1251
1272
  // The `favorite` capability is the single favoriting switch — propagate
1252
- // its match mode to the browser so it can hydrate row hearts.
1253
- browser?.browserManager?.setFavoriteMatch(options.capabilities.favoriteMatch)
1273
+ // it to the browser so it can hydrate row hearts.
1274
+ browser?.browserManager?.setFavoriteEnabled(options.capabilities.favoriteEnabled)
1254
1275
  }
1255
1276
 
1256
1277
  if (progressUpdateEventIntervalChanged) {
@@ -1,20 +1,7 @@
1
1
  package com.audiobrowser.player
2
2
 
3
- import com.margelo.nitro.audiobrowser.FavoritesMatchMode
4
3
  import com.margelo.nitro.audiobrowser.PlayerCapabilities
5
4
 
6
- /**
7
- * Resolved favorite match mode, or null when favoriting is disabled.
8
- *
9
- * `false`/unset → null; `true` → EXACT; `{ match }` → that match.
10
- */
11
- val PlayerCapabilities.favoriteMatch: FavoritesMatchMode?
12
- get() =
13
- favorite?.match(
14
- first = { enabled -> if (enabled) FavoritesMatchMode.EXACT else null },
15
- second = { config -> config.match },
16
- )
17
-
18
- /** Whether the favorite/like control is enabled. */
5
+ /** Whether the favorite/like control is enabled. Opt-in: only an explicit `true` enables it. */
19
6
  val PlayerCapabilities.favoriteEnabled: Boolean
20
- get() = favoriteMatch != null
7
+ get() = favorite == true
@@ -0,0 +1,30 @@
1
+ package com.audiobrowser.player
2
+
3
+ import androidx.media3.exoplayer.source.ShuffleOrder
4
+ import kotlin.random.Random
5
+
6
+ /**
7
+ * A shuffle order over [count] items with [startIndex] pinned to shuffle position 0.
8
+ *
9
+ * Media3 does not do this for us. `ExoPlayerImpl.setMediaSources` hands the start index to
10
+ * `ShuffleOrder.cloneAndSet(insertionCount, startIndex)`, but that is an interface default of
11
+ * `cloneAndClear().cloneAndInsert(0, insertionCount)` — the start index is discarded — and
12
+ * `DefaultShuffleOrder` does not override it (verified against the pinned media3 build). So with
13
+ * shuffle on, the track playback starts from lands at a random position in the order, and a 1-in-N
14
+ * share of queues put it last: `nextMediaItemIndex` is then `INDEX_UNSET` from the very first
15
+ * track, so auto-advance ends the queue after one track and fires `onQueueEnded`. The
16
+ * voice-assistant cold resume expands a queue exactly this way, so it sits squarely on that path.
17
+ *
18
+ * Applied unconditionally by `Player.setQueue`, like the Swift twin's `adoptInitialCurrent`:
19
+ * enabling shuffle later does not regenerate the order, so it has to be sane whether or not shuffle
20
+ * is on right now.
21
+ */
22
+ internal fun shuffleOrderLedBy(startIndex: Int, count: Int, random: Random = Random): ShuffleOrder {
23
+ val first = startIndex.coerceIn(0, (count - 1).coerceAtLeast(0))
24
+ val indices = IntArray(count)
25
+ if (count > 0) {
26
+ indices[0] = first
27
+ ((0 until count) - first).shuffled().forEachIndexed { i, index -> indices[i + 1] = index }
28
+ }
29
+ return ShuffleOrder.DefaultShuffleOrder(indices, random.nextLong())
30
+ }