react-native-audio-browser 0.2.0-next.10 → 0.2.0-next.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/README.md +4 -4
  2. package/android/build.gradle +6 -5
  3. package/android/gradle.properties +1 -1
  4. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
  5. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
  6. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  7. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -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 +49 -0
  10. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -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/player/MediaSessionCallback.kt +120 -67
  14. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  15. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
  16. package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
  17. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  18. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +46 -5
  19. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +86 -27
  20. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  21. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +20 -6
  22. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +249 -86
  23. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  24. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  25. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
  26. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
  27. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +69 -30
  28. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  29. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
  30. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +345 -8
  31. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  32. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  33. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
  34. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  35. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
  36. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  37. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  38. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  39. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  40. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  41. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +152 -8
  42. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +218 -10
  43. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  44. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  45. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  46. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  47. package/ios/Browser/BrowserConfig.swift +102 -5
  48. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  49. package/ios/Browser/BrowserManager.swift +131 -143
  50. package/ios/Browser/BrowserPathHelper.swift +44 -44
  51. package/ios/Browser/JsonModels.swift +187 -106
  52. package/ios/Browser/SearchDrillIn.swift +6 -6
  53. package/ios/Browser/SectionPresentation.swift +34 -0
  54. package/ios/Browser/SectionScope.swift +58 -24
  55. package/ios/Browser/StyleResolver.swift +47 -0
  56. package/ios/Browser/TabBarEntries.swift +2 -2
  57. package/ios/Browser/TrackIdTemplate.swift +37 -0
  58. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  59. package/ios/CarPlay/CarPlayController.swift +105 -67
  60. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  61. package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
  62. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  63. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  64. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  65. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  66. package/ios/Extension/Section+Copying.swift +31 -0
  67. package/ios/Extension/Track+Copying.swift +6 -12
  68. package/ios/Extension/Track+Identity.swift +18 -0
  69. package/ios/HybridAudioBrowser.swift +113 -35
  70. package/ios/Model/NitroTypeStubs.swift +64 -23
  71. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  72. package/ios/Player/NowPlayingUpdater.swift +1 -1
  73. package/ios/Player/PlaybackCoordinator.swift +2 -2
  74. package/ios/Player/RetryManager.swift +4 -3
  75. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  76. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  77. package/ios/Tests/JsonModelsTests.swift +75 -1
  78. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  79. package/ios/Tests/QueueManagerTests.swift +4 -4
  80. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  81. package/ios/Tests/SearchDrillInTests.swift +16 -16
  82. package/ios/Tests/SectionPresentationTests.swift +61 -0
  83. package/ios/Tests/SectionScopeTests.swift +87 -55
  84. package/ios/Tests/StyleResolverTests.swift +67 -0
  85. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  86. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  87. package/ios/Tests/TrackIdentityTests.swift +20 -0
  88. package/ios/Tests/TrackSelectorTests.swift +168 -33
  89. package/ios/TrackSelector.swift +45 -30
  90. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  91. package/lib/commonjs/AudioBrowser.js +11 -0
  92. package/lib/commonjs/AudioBrowser.js.map +1 -1
  93. package/lib/commonjs/features/browser-config.js +11 -8
  94. package/lib/commonjs/features/browser-config.js.map +1 -1
  95. package/lib/commonjs/features/browser.js +2 -2
  96. package/lib/commonjs/features/favorites.js +13 -10
  97. package/lib/commonjs/features/favorites.js.map +1 -1
  98. package/lib/commonjs/features/playback/controls.js +4 -2
  99. package/lib/commonjs/features/playback/controls.js.map +1 -1
  100. package/lib/commonjs/features/playback/state.js +2 -3
  101. package/lib/commonjs/features/playback/state.js.map +1 -1
  102. package/lib/commonjs/features/player/options.js.map +1 -1
  103. package/lib/commonjs/features/player/setup.js +3 -1
  104. package/lib/commonjs/features/player/setup.js.map +1 -1
  105. package/lib/commonjs/features/player/validateOptions.js +2 -0
  106. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  107. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  108. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  109. package/lib/commonjs/features/queue/queue.js +11 -4
  110. package/lib/commonjs/features/queue/queue.js.map +1 -1
  111. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  112. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  113. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  114. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  115. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  116. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  117. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  118. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  119. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  120. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  121. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  122. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  123. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  124. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  125. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  126. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  127. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  128. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  129. package/lib/commonjs/web/browser/sections.js +89 -0
  130. package/lib/commonjs/web/browser/sections.js.map +1 -0
  131. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  132. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  133. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  134. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  135. package/lib/module/AudioBrowser.js +1 -0
  136. package/lib/module/AudioBrowser.js.map +1 -1
  137. package/lib/module/features/browser-config.js +11 -8
  138. package/lib/module/features/browser-config.js.map +1 -1
  139. package/lib/module/features/browser.js +2 -2
  140. package/lib/module/features/favorites.js +13 -10
  141. package/lib/module/features/favorites.js.map +1 -1
  142. package/lib/module/features/playback/controls.js +4 -2
  143. package/lib/module/features/playback/controls.js.map +1 -1
  144. package/lib/module/features/playback/state.js +2 -3
  145. package/lib/module/features/playback/state.js.map +1 -1
  146. package/lib/module/features/player/options.js.map +1 -1
  147. package/lib/module/features/player/setup.js +3 -1
  148. package/lib/module/features/player/setup.js.map +1 -1
  149. package/lib/module/features/player/validateOptions.js +2 -0
  150. package/lib/module/features/player/validateOptions.js.map +1 -1
  151. package/lib/module/features/queue/activeTrack.js +14 -3
  152. package/lib/module/features/queue/activeTrack.js.map +1 -1
  153. package/lib/module/features/queue/queue.js +11 -4
  154. package/lib/module/features/queue/queue.js.map +1 -1
  155. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  156. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  157. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  158. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  159. package/lib/module/utils/getTrackIdentity.js +20 -0
  160. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  161. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  162. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  163. package/lib/module/web/NativeAudioBrowser.js +32 -24
  164. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  165. package/lib/module/web/browser/BrowserManager.js +144 -87
  166. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  167. package/lib/module/web/browser/FavoriteManager.js +31 -21
  168. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  169. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  170. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  171. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  172. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  173. package/lib/module/web/browser/sections.js +83 -0
  174. package/lib/module/web/browser/sections.js.map +1 -0
  175. package/lib/module/web/player/NowPlayingManager.js +4 -3
  176. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  177. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  178. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  179. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  180. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  181. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  182. package/lib/typescript/src/features/browser.d.ts +2 -2
  183. package/lib/typescript/src/features/favorites.d.ts +13 -10
  184. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  185. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  186. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  187. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  188. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  189. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  190. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  191. package/lib/typescript/src/features/player/options.d.ts +21 -19
  192. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  193. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  194. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  195. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  196. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  197. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  198. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  199. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  200. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  201. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  202. package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
  203. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  204. package/lib/typescript/src/types/browser.d.ts +33 -53
  205. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  206. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  207. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  208. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  209. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  210. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  211. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  212. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  213. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  214. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  215. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  216. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
  217. package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
  218. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  219. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
  220. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
  221. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
  222. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
  223. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
  224. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
  225. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
  226. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
  227. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +10 -4
  228. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
  229. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  230. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
  231. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
  232. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
  233. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
  234. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  235. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
  236. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
  237. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
  238. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
  239. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  240. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  241. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  242. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
  243. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  244. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
  245. package/nitrogen/generated/android/c++/JSection.hpp +114 -0
  246. package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
  247. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  248. package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
  249. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
  250. package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
  251. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
  252. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  253. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  254. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  255. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  256. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  257. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  258. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  259. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
  260. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
  261. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  262. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  263. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
  264. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
  265. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
  266. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
  267. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  268. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  269. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  270. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  271. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  272. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  273. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  274. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  275. package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
  276. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  277. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  278. package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
  279. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  280. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  281. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  282. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  283. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  284. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  285. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  286. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  287. package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
  288. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  289. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  290. package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
  291. package/package.json +3 -2
  292. package/src/AudioBrowser.ts +1 -0
  293. package/src/features/browser-config.test.ts +21 -6
  294. package/src/features/browser-config.ts +11 -8
  295. package/src/features/browser.test.ts +1 -1
  296. package/src/features/browser.ts +2 -2
  297. package/src/features/favorites.ts +13 -10
  298. package/src/features/hook-wiring.test.ts +63 -15
  299. package/src/features/playback/controls.ts +4 -2
  300. package/src/features/playback/state.ts +2 -3
  301. package/src/features/playback/trackPlaybackTime.ts +1 -1
  302. package/src/features/player/options.ts +27 -19
  303. package/src/features/player/setup.test.ts +12 -3
  304. package/src/features/player/setup.ts +3 -1
  305. package/src/features/player/validateOptions.ts +2 -0
  306. package/src/features/queue/activeTrack.ts +14 -3
  307. package/src/features/queue/queue.ts +11 -4
  308. package/src/nitro-stub-drift.test.ts +277 -0
  309. package/src/public-surface.test.ts +40 -0
  310. package/src/specs/audio-browser.nitro.ts +4 -3
  311. package/src/style-resolution.test.ts +139 -0
  312. package/src/types/browser-native.ts +2 -2
  313. package/src/types/browser-nodes.ts +268 -136
  314. package/src/types/browser.ts +34 -55
  315. package/src/utils/LazyNativeEmitter.ts +20 -10
  316. package/src/utils/NativeUpdatedValue.ts +29 -13
  317. package/src/utils/getTrackIdentity.ts +20 -0
  318. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  319. package/src/web/NativeAudioBrowser.ts +37 -22
  320. package/src/web/browser/BrowserManager.ts +176 -95
  321. package/src/web/browser/FavoriteManager.test.ts +84 -0
  322. package/src/web/browser/FavoriteManager.ts +28 -23
  323. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  324. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  325. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  326. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  327. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  328. package/src/web/browser/sections.test.ts +91 -0
  329. package/src/web/browser/sections.ts +72 -0
  330. package/src/web/player/NowPlayingManager.ts +4 -3
  331. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  332. package/src/web/util/BrowserPathHelper.ts +61 -13
  333. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  334. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  335. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  336. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  337. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  338. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  339. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  340. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  341. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  342. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  343. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  344. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  345. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -11,49 +11,43 @@ extension Track {
11
11
  /// `title` uses single-optional (`String?`) since it is non-optional on Track.
12
12
  func copying(
13
13
  id: String?? = nil,
14
- url: String?? = nil,
14
+ path: String?? = nil,
15
15
  src: String?? = nil,
16
16
  artwork: Variant_String_ArtworkVariants?? = nil,
17
17
  artworkSource: ImageSource?? = nil,
18
18
  request: TrackRequest?? = nil,
19
- artworkCarPlayTinted: Bool?? = nil,
20
19
  title: String? = nil,
21
20
  subtitle: String?? = nil,
22
21
  artist: String?? = nil,
23
- albumUrl: String?? = nil,
22
+ albumPath: String?? = nil,
24
23
  album: String?? = nil,
25
24
  description: String?? = nil,
26
25
  genre: String?? = nil,
27
26
  duration: Double?? = nil,
28
27
  style: TrackStyle?? = nil,
29
- childrenStyle: TrackStyle?? = nil,
28
+ disabled: Bool?? = nil,
30
29
  favorited: Bool?? = nil,
31
- groupTitle: String?? = nil,
32
30
  live: Bool?? = nil,
33
- imageRow: [ImageRowItem]?? = nil,
34
31
  ) -> Track {
35
32
  Track(
36
33
  id: id ?? self.id,
37
- url: url ?? self.url,
34
+ path: path ?? self.path,
38
35
  src: src ?? self.src,
39
36
  artwork: artwork ?? self.artwork,
40
37
  artworkSource: artworkSource ?? self.artworkSource,
41
38
  request: request ?? self.request,
42
- artworkCarPlayTinted: artworkCarPlayTinted ?? self.artworkCarPlayTinted,
43
39
  title: title ?? self.title,
44
40
  subtitle: subtitle ?? self.subtitle,
45
41
  artist: artist ?? self.artist,
46
- albumUrl: albumUrl ?? self.albumUrl,
42
+ albumPath: albumPath ?? self.albumPath,
47
43
  album: album ?? self.album,
48
44
  description: description ?? self.description,
49
45
  genre: genre ?? self.genre,
50
46
  duration: duration ?? self.duration,
51
47
  style: style ?? self.style,
52
- childrenStyle: childrenStyle ?? self.childrenStyle,
48
+ disabled: disabled ?? self.disabled,
53
49
  favorited: favorited ?? self.favorited,
54
- groupTitle: groupTitle ?? self.groupTitle,
55
50
  live: live ?? self.live,
56
- imageRow: imageRow ?? self.imageRow,
57
51
  )
58
52
  }
59
53
  }
@@ -0,0 +1,18 @@
1
+ #if canImport(NitroModules)
2
+ import NitroModules
3
+ #endif
4
+
5
+ /// A track's identity: the opaque `id` when present (non-blank), else the
6
+ /// playable `src`. Two tracks refer to the same item iff their identities are
7
+ /// equal. Browsable-only tracks (neither `id` nor `src`) have no identity —
8
+ /// they are addressed by `path` instead.
9
+ ///
10
+ /// This is THE comparison rule for favorites matching, section scoping,
11
+ /// skip-in-place, the car now-playing row indicator, and the contextual
12
+ /// `__trackId` — see ADR 0008. Mirrors `trackIdentity` on the TS side.
13
+ extension Track {
14
+ var identity: String? {
15
+ if let id, !id.isEmpty { return id }
16
+ return src
17
+ }
18
+ }
@@ -34,6 +34,24 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
34
34
  /// silent.
35
35
  static let instanceChangedEmitter = Emitter<Void>()
36
36
 
37
+ /// Installed once, on first instance creation. React Native posts this
38
+ /// notification (from `RCTReloadCommand`) when a JS reload — dev or
39
+ /// OTA-update restart — is triggered, *before* the old runtime is destroyed:
40
+ /// the earliest signal that this instance's JS callbacks are about to go
41
+ /// stale. `init` of the replacement instance drops them too, but only after
42
+ /// the new runtime is up; this observer closes the window in between.
43
+ /// Observed by name so the library needs no React-Core dependency (and
44
+ /// stays inert under SPM tests, where nothing posts it).
45
+ @MainActor private static let reloadObserver: NSObjectProtocol = NotificationCenter.default.addObserver(
46
+ forName: Notification.Name("RCTTriggerReloadCommandNotification"),
47
+ object: nil,
48
+ queue: .main,
49
+ ) { _ in
50
+ MainActor.assumeIsolated {
51
+ HybridAudioBrowser.shared?.dropStaleJSCallbacks()
52
+ }
53
+ }
54
+
37
55
  // MARK: - Private Properties
38
56
 
39
57
  /// Main-confined by convention, not annotation: `deinit` reads it as a
@@ -70,6 +88,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
70
88
  playerOptions.carPlayNowPlayingButtons
71
89
  }
72
90
 
91
+ @MainActor var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation {
92
+ playerOptions.carPlayPlayingIndicatorLocation
93
+ }
94
+
73
95
  /// Written from the JS thread, the cooperative pool and MainActor, with a
74
96
  /// `didSet` that calls back into Nitro. MainActor-isolated so the compiler
75
97
  /// requires an `onMainActor` hop at every access.
@@ -164,7 +186,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
164
186
  public var configuration: NativeBrowserConfiguration = .init(
165
187
  path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
166
188
  singleTrack: nil, handleTrackLoad: nil,
167
- androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumUrl: nil, formatNavigationError: nil,
189
+ androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumPath: nil, formatNavigationError: nil,
168
190
  ) {
169
191
  didSet {
170
192
  // Copied here, on the setter's thread: the Task below outlives this call,
@@ -181,7 +203,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
181
203
  Task {
182
204
  let tabs = try? await browserManager.queryTabs()
183
205
  // Navigate to configured path, first tab, or "/"
184
- let initialPath = configuredPath ?? tabs?.first?.url ?? "/"
206
+ let initialPath = configuredPath ?? tabs?.first?.path ?? "/"
185
207
  // Clear error before navigation (matches Kotlin clearNavigationError())
186
208
  clearNavigationError()
187
209
  do {
@@ -213,8 +235,14 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
213
235
  /// state, e.g. CarPlay re-seeding `isGated` at scene connect). Serving content
214
236
  /// in that window would be a fail-open leak — same direction as the
215
237
  /// resolver-error path in `gateDecision`. Matches Android's default.
216
- public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = { _ in
217
- Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
238
+ public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = HybridAudioBrowser.defaultResolveGate
239
+
240
+ // Computed, not stored: a nonisolated static holding a non-Sendable closure
241
+ // is rejected under Swift 6 concurrency checking.
242
+ static var defaultResolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> {
243
+ { _ in
244
+ Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
245
+ }
218
246
  }
219
247
 
220
248
  /// Fired when a request is gated (served the gate). Set by JS.
@@ -306,7 +334,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
306
334
  // playerAndConfiguredBrowser OnceValue, so we must explicitly stop
307
335
  // its player to prevent two audio streams running simultaneously.
308
336
  onMainActor {
337
+ // Statics are lazy — touch the observer so it's installed from the first
338
+ // instance onward.
339
+ _ = HybridAudioBrowser.reloadObserver
309
340
  HybridAudioBrowser.shared?.player?.destroy()
341
+ HybridAudioBrowser.shared?.dropStaleJSCallbacks()
310
342
  playerAndConfiguredBrowser.reset()
311
343
  HybridAudioBrowser.shared = self
312
344
  // Tell connected external controllers (CarPlay) to re-subscribe against
@@ -319,6 +351,38 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
319
351
  setupBrowserCallbacks()
320
352
  }
321
353
 
354
+ /// Drops every JS-provided callback that native code can invoke on its own
355
+ /// initiative (browse resolvers, `handleTrackLoad`, `resolveAlbumPath`,
356
+ /// `formatNavigationError`, `resolveGate`, the now-playing formatter).
357
+ ///
358
+ /// Called on the *previous* instance when a new one takes over: its JS runtime
359
+ /// is gone, and invoking a callback from a dead runtime aborts the process —
360
+ /// Nitro's `AsyncJSCallback` throws on a destroyed Dispatcher, and the throw
361
+ /// crosses the generated bridge wrapper's `noexcept` boundary. Native code
362
+ /// (e.g. a CarPlay controller still bound to this instance mid-reload) keeps
363
+ /// working off the data-only config with built-in fallbacks; `resolveGate`
364
+ /// reverts to the fail-closed default it documents for exactly this window.
365
+ /// One-shot: on a normal reload both triggers hit the same dying instance
366
+ /// (the reload notification, then the replacement's `init`), and the second
367
+ /// strip would re-fire the config-setter cascade — another `onConfigChanged`
368
+ /// → CarPlay now-playing button rebuild — for nothing.
369
+ @MainActor private var didDropJSCallbacks = false
370
+
371
+ @MainActor
372
+ private func dropStaleJSCallbacks() {
373
+ guard !didDropJSCallbacks else { return }
374
+ didDropJSCallbacks = true
375
+ // Guarded: assigning `config` marks the browser configured (the setter's
376
+ // didSet), and a never-configured browser must keep cold-start waiters
377
+ // (CarPlay, Siri) blocked on the readiness gate rather than resolve them
378
+ // against an empty config.
379
+ if browserManager.isConfigured {
380
+ browserManager.config = browserManager.config.strippingJSCallbacks()
381
+ }
382
+ resolveGate = Self.defaultResolveGate
383
+ nowPlayingMetadataFormatter = nil
384
+ }
385
+
322
386
  deinit {
323
387
  // Safety net: ensure the AVPlayer is stopped if this instance is deallocated.
324
388
  let player = self.player
@@ -447,8 +511,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
447
511
  executePlayback(intent)
448
512
  case .intercepted:
449
513
  break
450
- case let .browse(url):
451
- navigateToBrowsableUrl(url)
514
+ case let .browse(path):
515
+ navigateToBrowsablePath(path)
452
516
  case .none:
453
517
  break
454
518
  }
@@ -472,13 +536,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
472
536
  }
473
537
  }
474
538
 
475
- private func navigateToBrowsableUrl(_ url: String) {
539
+ private func navigateToBrowsablePath(_ path: String) {
476
540
  clearNavigationError()
477
541
  Task {
478
542
  do {
479
- try await browserManager.navigate(url)
543
+ try await browserManager.navigate(path)
480
544
  } catch {
481
- handleNavigationError(error, path: url)
545
+ handleNavigationError(error, path: path)
482
546
  }
483
547
  }
484
548
  }
@@ -487,7 +551,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
487
551
  Promise.async { [weak self] in
488
552
  guard let self else { return [] }
489
553
  let resolved = try await browserManager.search(query)
490
- return resolved.children ?? []
554
+ return resolved.flattenedChildren ?? []
491
555
  }
492
556
  }
493
557
 
@@ -739,11 +803,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
739
803
  return await browserManager.resolveMediaUrl(src, track: track)
740
804
  }
741
805
 
742
- // Configure artwork URL resolver for Now Playing (with size context)
806
+ // Configure artwork URL resolver for Now Playing (with size context). The
807
+ // `nowPlayingArtwork` config runs for EVERY playing track, id or not — an id-less track
808
+ // leaves any `{id}` token unfilled (resolveArtworkUrl warns and the request visibly
809
+ // 404s) rather than skipping the config.
743
810
  player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
744
811
  guard let self else { return nil }
745
- let perRoute = (track.id?.isEmpty == false) ? browserManager.config.nowPlayingArtwork : nil
746
- return await browserManager.resolveArtworkUrl(track: track, perRouteConfig: perRoute, imageContext: imageContext)
812
+ return await browserManager.resolveArtworkUrl(
813
+ track: track, perRouteConfig: browserManager.config.nowPlayingArtwork,
814
+ imageContext: imageContext,
815
+ )
747
816
  }
748
817
 
749
818
  // Emit the JS `onNowPlayingChanged` event when the rendered text lines change. The updater
@@ -813,9 +882,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
813
882
  // Update stored options
814
883
  playerOptions.update(from: options)
815
884
 
816
- // Propagate the favorite match mode to the browser so it can hydrate
885
+ // Propagate the favorite flag to the browser so it can hydrate
817
886
  // row hearts (the `favorite` capability is the single favoriting switch).
818
- browserManager.setFavoriteMatch(playerOptions.capabilities.favoriteMatch)
887
+ browserManager.setFavoriteEnabled(playerOptions.capabilities.favoriteEnabled)
819
888
 
820
889
  // Apply remote commands to player
821
890
  applyRemoteCommands()
@@ -1064,23 +1133,24 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1064
1133
 
1065
1134
  public func setActiveTrackFavorited(favorited: Bool) throws {
1066
1135
  onMainActor {
1067
- guard let track = player?.currentTrack, let src = track.src else { return }
1136
+ guard let track = player?.currentTrack, let identity = track.identity else { return }
1068
1137
  guard let index = player?.currentIndex, index >= 0 else { return }
1069
- // Optimistically reflect in the authoritative match set so the now-playing
1070
- // heart (hydrated from it in getActiveTrack) flips immediately; native
1071
- // reconciles to its canonical ids on the next setFavorites.
1072
- browserManager.setFavorited(src: src, favorited: favorited)
1138
+ // Optimistically reflect in the authoritative favorite set so the
1139
+ // now-playing heart (hydrated from it in getActiveTrack) flips
1140
+ // immediately; the consumer reconciles to its canonical identities on
1141
+ // the next setFavorites.
1142
+ browserManager.setFavorited(identity: identity, favorited: favorited)
1073
1143
  let updatedTrack = track.copying(favorited: favorited)
1144
+ // Hydration never overrides a caller-set `favorited`, so the queued
1145
+ // track must itself carry the latest explicit value (as Android's tag
1146
+ // swap and web's replaceTrack do).
1147
+ player?.replace(index, updatedTrack)
1148
+ // The ONLY emit: a favorite toggle is an in-place mutation of the active
1149
+ // track, not a transition — onPlaybackActiveTrackChanged stays
1150
+ // transition-only, and the useActiveTrack / useQueue hooks subscribe to
1151
+ // onFavoriteChanged themselves. Native surfaces (CarPlay heart) observe
1152
+ // favoriteChangedEmitter / onFavoritesChanged directly.
1074
1153
  favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
1075
- // Fire active track changed so the now-playing heart + useActiveTrack() refresh.
1076
- let position = player?.currentTime ?? 0
1077
- activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
1078
- lastIndex: Double(index),
1079
- lastTrack: track,
1080
- lastPosition: position,
1081
- index: Double(index),
1082
- track: updatedTrack,
1083
- ))
1084
1154
  }
1085
1155
  }
1086
1156
 
@@ -1220,7 +1290,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1220
1290
  duration: track.duration,
1221
1291
  artwork: nowPlayingArtwork(for: track),
1222
1292
  description: track.description,
1223
- mediaId: track.src ?? track.url,
1293
+ mediaId: track.src ?? track.path,
1224
1294
  genre: track.genre,
1225
1295
  )
1226
1296
  }
@@ -1809,14 +1879,22 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1809
1879
  browser.showNowPlayingRequestedEmitter.emit(())
1810
1880
  completion(true)
1811
1881
  } else if let state = browser.playbackStateStore.load() {
1812
- browser.logger.info("resume: cold — restoring persisted track")
1813
1882
  let track = state.track.toNitro()
1883
+ // The persisted track can carry `disabled` — it became unavailable
1884
+ // after it was saved. A disabled track never plays, resumption
1885
+ // included (Track.disabled).
1886
+ if track.disabled == true {
1887
+ browser.logger.info("resume: persisted track is disabled — refusing")
1888
+ completion(false)
1889
+ return
1890
+ }
1891
+ browser.logger.info("resume: cold — restoring persisted track")
1814
1892
  let startMs = (track.live == true) ? nil : state.positionMs
1815
1893
  // Match Android resume: re-expand the full queue from the track's contextual
1816
- // URL (parent container → siblings + selected index). Fall back to the single
1817
- // track when the url isn't contextual or expansion fails.
1818
- if let url = state.track.url,
1819
- let expanded = try? await browser.browserManager.expandQueueFromContextualUrl(url)
1894
+ // path (parent container → siblings + selected index). Fall back to the single
1895
+ // track when the path isn't contextual or expansion fails.
1896
+ if let path = state.track.path,
1897
+ let expanded = try? await browser.browserManager.expandQueueFromContextualPath(path)
1820
1898
  {
1821
1899
  player.setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
1822
1900
  } else {
@@ -5,6 +5,12 @@
5
5
  /// These stubs declare `Equatable` for test ergonomics — the generated
6
6
  /// Nitro types do NOT. Shared `ios/` sources must not rely on conformances
7
7
  /// that exist only here: `swift test` compiles, the app build breaks.
8
+ ///
9
+ /// Property names and types ARE checked against `nitrogen/generated/ios/swift`
10
+ /// by `src/nitro-stub-drift.test.ts`, which runs in the JS CI job — neither
11
+ /// native job can see both files at once. Declaring fewer properties than the
12
+ /// real type is fine and deliberate; spelling one differently is what that
13
+ /// test catches. Conformances and default values are still on you.
8
14
 
9
15
  struct ArtworkVariants: Equatable {
10
16
  var light: String
@@ -23,19 +29,19 @@
23
29
  }
24
30
 
25
31
  struct Track: Equatable {
26
- var id: String
27
- var url: String?
32
+ var id: String?
33
+ var path: String?
28
34
  var src: String?
35
+ var artwork: Variant_String_ArtworkVariants?
36
+ var artworkSource: ImageSource?
29
37
  var request: TrackRequest?
30
38
  var title: String = ""
31
39
  var artist: String?
32
- var albumUrl: String?
40
+ var albumPath: String?
33
41
  var album: String?
42
+ var style: TrackStyle?
43
+ var disabled: Bool?
34
44
  var live: Bool?
35
- var artwork: Variant_String_ArtworkVariants?
36
- var artworkSource: ImageSource?
37
- var groupTitle: String?
38
- var imageRow: [ImageRowItem]?
39
45
  }
40
46
 
41
47
  struct TrackLoadEvent {
@@ -63,14 +69,24 @@
63
69
  }
64
70
  }
65
71
 
72
+ enum HttpMethod {
73
+ case get
74
+ case post
75
+ case put
76
+ case patch
77
+ case delete
78
+ case head
79
+ case options
80
+ }
81
+
66
82
  struct ImageSource: Equatable {
67
83
  var uri: String
68
- var method: String?
84
+ var method: HttpMethod?
69
85
  var headers: [String: String]?
70
86
  var body: String?
71
87
  }
72
88
 
73
- enum TrackStyle {
89
+ enum StyleDisplay: Equatable {
74
90
  case list
75
91
  case grid
76
92
 
@@ -83,17 +99,46 @@
83
99
  }
84
100
  }
85
101
 
86
- struct ImageRowItem: Equatable {
87
- var id: String?
88
- var url: String?
89
- var src: String?
90
- var artwork: String?
91
- var artworkSource: ImageSource?
92
- var title: String
102
+ enum ArtworkRendering: Equatable {
103
+ case original
104
+ case stencil
105
+
106
+ init?(fromString string: String) {
107
+ switch string {
108
+ case "original": self = .original
109
+ case "stencil": self = .stencil
110
+ default: return nil
111
+ }
112
+ }
113
+ }
114
+
115
+ struct TrackStyle: Equatable {
116
+ var display: StyleDisplay?
117
+ var artworkRendering: ArtworkRendering?
118
+ }
119
+
120
+ // Member order matches the generated init (own properties before inherited
121
+ // ones — Nitro flattens `extends` own-props-first): the memberwise init this
122
+ // produces must accept the same argument order as the real type, or
123
+ // `swift test` passes while the app build breaks on argument order.
124
+ struct SectionStyle: Equatable {
125
+ var gridWrap: Bool?
126
+ var display: StyleDisplay?
127
+ var artworkRendering: ArtworkRendering?
128
+ }
129
+
130
+ struct Section: Equatable {
131
+ var title: String?
132
+ var subtitle: String?
133
+ var style: SectionStyle?
134
+ var path: String?
135
+ var children: [Track]
93
136
  }
94
137
 
95
138
  struct ResolvedTrack: Equatable {
96
- var url: String
139
+ var path: String
140
+ var style: SectionStyle?
141
+ var sections: [Section]?
97
142
  var children: [Track]?
98
143
  var carPlaySiriListButton: CarPlaySiriListButtonPosition?
99
144
  var id: String?
@@ -101,21 +146,17 @@
101
146
  var artwork: Variant_String_ArtworkVariants?
102
147
  var artworkSource: ImageSource?
103
148
  var request: TrackRequest?
104
- var artworkCarPlayTinted: Bool?
105
149
  var title: String
106
150
  var subtitle: String?
107
151
  var artist: String?
108
- var albumUrl: String?
152
+ var albumPath: String?
109
153
  var album: String?
110
154
  var description: String?
111
155
  var genre: String?
112
156
  var duration: Double?
113
- var style: TrackStyle?
114
- var childrenStyle: TrackStyle?
157
+ var disabled: Bool?
115
158
  var favorited: Bool?
116
- var groupTitle: String?
117
159
  var live: Bool?
118
- var imageRow: [ImageRowItem]?
119
160
  }
120
161
 
121
162
  enum PlaybackState: Equatable {
@@ -32,6 +32,9 @@ class PlayerUpdateOptions {
32
32
  /// Custom buttons for the CarPlay Now Playing screen
33
33
  var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] = []
34
34
 
35
+ /// Where the now-playing indicator draws on CarPlay list rows
36
+ var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation = .leading
37
+
35
38
  // MARK: - Initialization
36
39
 
37
40
  init() {}
@@ -74,6 +77,9 @@ class PlayerUpdateOptions {
74
77
  if let buttons = ios.carPlayNowPlayingButtons {
75
78
  carPlayNowPlayingButtons = buttons
76
79
  }
80
+ if let location = ios.carPlayPlayingIndicatorLocation {
81
+ carPlayPlayingIndicatorLocation = location
82
+ }
77
83
  }
78
84
  }
79
85
 
@@ -92,6 +98,7 @@ class PlayerUpdateOptions {
92
98
  playbackRates: playbackRates,
93
99
  carPlayUpNextButton: carPlayUpNextButton,
94
100
  carPlayNowPlayingButtons: carPlayNowPlayingButtons,
101
+ carPlayPlayingIndicatorLocation: carPlayPlayingIndicatorLocation,
95
102
  ),
96
103
  )
97
104
  }
@@ -130,7 +130,7 @@ final class NowPlayingUpdater {
130
130
  let resolvedArtist = artist ?? track.artist
131
131
  let resolvedAlbum = album ?? track.album
132
132
  let published = Published(
133
- trackId: track.src ?? track.url,
133
+ trackId: track.identity ?? track.path,
134
134
  title: resolvedTitle,
135
135
  artist: resolvedArtist,
136
136
  album: resolvedAlbum,
@@ -691,14 +691,14 @@ class PlaybackCoordinator {
691
691
  guard let src = currentTrack.src else {
692
692
  logger.error("Failed to load track - no src")
693
693
  logger.error(" track.title: \(currentTrack.title)")
694
- logger.error(" track.url: \(currentTrack.url ?? "nil")")
694
+ logger.error(" track.path: \(currentTrack.path ?? "nil")")
695
695
  effectHandler?.clearCurrentItem()
696
696
  transition(.errorOccurred(.invalidSourceUrl("nil")))
697
697
  return
698
698
  }
699
699
 
700
700
  logger.debug("Loading track: \(currentTrack.title)")
701
- logger.debug(" track.url: \(currentTrack.url ?? "nil")")
701
+ logger.debug(" track.path: \(currentTrack.path ?? "nil")")
702
702
  logger.debug(" track.src: \(src)")
703
703
 
704
704
  effectHandler?.loadTrack(src: src, track: currentTrack)
@@ -123,9 +123,10 @@ class RetryManager {
123
123
 
124
124
  // MARK: - Error Classification
125
125
 
126
- /// HTTP statuses worth another attempt. Same set as Android's
127
- /// `RetryLoadErrorHandlingPolicy.RETRYABLE_HTTP_STATUS_CODES`, so a flaky
128
- /// server behaves the same on both platforms.
126
+ /// HTTP statuses worth another attempt, beyond the 500–599 range checked in
127
+ /// `isRetryable`. Android's `RetryLoadErrorHandlingPolicy` shares 408/429 but
128
+ /// enumerates only 500/502/503/504 of the 5xx range, so uncommon 5xx codes
129
+ /// (501, 505…) retry here and not there.
129
130
  private static let retryableHTTPStatusCodes: Set<Int> = [408, 429]
130
131
 
131
132
  /// AVFoundation failures a retry cannot fix: the media is unusable rather
@@ -74,6 +74,56 @@ import Testing
74
74
  #expect(result == "/items?a=1&m=2&z=3")
75
75
  }
76
76
 
77
+ // MARK: - build
78
+
79
+ @Test func buildAppendsTrackIdWithQuestionMark() {
80
+ let result = BrowserPathHelper.build(parentPath: "/library", trackId: "song.mp3")
81
+ #expect(result == "/library?__trackId=song.mp3")
82
+ }
83
+
84
+ @Test func buildAppendsTrackIdWithAmpersandWhenParentHasQuery() {
85
+ let result = BrowserPathHelper.build(parentPath: "/search?q=jazz", trackId: "song.mp3")
86
+ #expect(result == "/search?q=jazz&__trackId=song.mp3")
87
+ }
88
+
89
+ @Test func buildRoundTripsSrcWithQueryParams() {
90
+ // A src carrying its own query string (signed CDN URL) must survive the
91
+ // build → extract/strip round-trip: an unescaped `&` splits the src into
92
+ // stray query params, truncating the trackId and polluting the parent path.
93
+ let src = "https://cdn.example.com/stream.mp3?token=abc&exp=1699999999"
94
+ let url = BrowserPathHelper.build(parentPath: "/library", trackId: src)
95
+ #expect(BrowserPathHelper.extractTrackId(url) == src)
96
+ #expect(BrowserPathHelper.stripTrackId(url) == "/library")
97
+ }
98
+
99
+ @Test func buildRoundTripsSrcWithEqualsAndPlus() {
100
+ let src = "https://cdn.example.com/a+b.mp3?sig=x=y"
101
+ let url = BrowserPathHelper.build(parentPath: "/library", trackId: src)
102
+ #expect(BrowserPathHelper.extractTrackId(url) == src)
103
+ #expect(BrowserPathHelper.stripTrackId(url) == "/library")
104
+ }
105
+
106
+ @Test func buildStampsTheIndexAndRoundTripsIt() {
107
+ let src = "https://cdn.example.com/stream.mp3?token=abc&exp=1"
108
+ let url = BrowserPathHelper.build(parentPath: "/library", trackId: src, index: 3)
109
+ #expect(BrowserPathHelper.extractTrackId(url) == src)
110
+ #expect(BrowserPathHelper.extractIndex(url) == 3)
111
+ #expect(BrowserPathHelper.stripTrackId(url) == "/library")
112
+ }
113
+
114
+ // MARK: - extractIndex
115
+
116
+ @Test func extractIndexReturnsNilWithoutAStampedIndex() {
117
+ let url = BrowserPathHelper.build(parentPath: "/library", trackId: "song.mp3")
118
+ #expect(BrowserPathHelper.extractIndex(url) == nil)
119
+ }
120
+
121
+ @Test func extractIndexReturnsNilForNonContextualOrMalformedIndex() {
122
+ #expect(BrowserPathHelper.extractIndex("/library") == nil)
123
+ #expect(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=x") == nil)
124
+ #expect(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=-1") == nil)
125
+ }
126
+
77
127
  // MARK: - stripTrackId
78
128
 
79
129
  @Test func stripTrackIdRemovesTrackParam() {
@@ -116,23 +166,3 @@ import Testing
116
166
  @Test func isContextualReturnsTrueWithAmpersandSeparator() {
117
167
  #expect(BrowserPathHelper.isContextual("/lib?q=1&__trackId=x") == true)
118
168
  }
119
-
120
- // MARK: - containsSegment
121
-
122
- @Test func containsSegmentMatchesFullSegments() {
123
- let path = "/listen/amsterdam-funk-channel/Gw0LGB8j"
124
- #expect(BrowserPathHelper.containsSegment(path, "Gw0LGB8j"))
125
- #expect(BrowserPathHelper.containsSegment(path, "amsterdam-funk-channel"))
126
- #expect(BrowserPathHelper.containsSegment(path, "listen"))
127
- #expect(BrowserPathHelper.containsSegment("/listen/x/Gw0LGB8j?hl=en", "Gw0LGB8j"))
128
- #expect(BrowserPathHelper.containsSegment("/listen/x/Gw0LGB8j#frag", "Gw0LGB8j"))
129
- }
130
-
131
- @Test func containsSegmentRejectsPartialAndMissing() {
132
- let path = "/listen/amsterdam-funk-channel/Gw0LGB8j"
133
- #expect(!BrowserPathHelper.containsSegment(path, "Gw0LGB8"))
134
- #expect(!BrowserPathHelper.containsSegment(path, "funk"))
135
- #expect(!BrowserPathHelper.containsSegment(path, "msterdam-funk-channe"))
136
- #expect(!BrowserPathHelper.containsSegment(path, "NopeNope"))
137
- #expect(!BrowserPathHelper.containsSegment(path, ""))
138
- }