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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/README.md +4 -4
  2. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
  3. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  5. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
  6. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
  7. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
  8. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -7
  9. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  10. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  11. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
  12. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  13. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
  14. package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
  15. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  16. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  17. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  18. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
  19. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
  20. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +26 -39
  21. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  22. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +228 -50
  23. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  24. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  25. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
  26. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  27. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
  28. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  29. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  30. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  31. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  32. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  33. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  34. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  35. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  36. package/ios/Browser/BrowserConfig.swift +102 -5
  37. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  38. package/ios/Browser/BrowserManager.swift +131 -143
  39. package/ios/Browser/BrowserPathHelper.swift +44 -44
  40. package/ios/Browser/JsonModels.swift +187 -106
  41. package/ios/Browser/SearchDrillIn.swift +6 -6
  42. package/ios/Browser/SectionPresentation.swift +34 -0
  43. package/ios/Browser/SectionScope.swift +58 -24
  44. package/ios/Browser/StyleResolver.swift +47 -0
  45. package/ios/Browser/TabBarEntries.swift +2 -2
  46. package/ios/Browser/TrackIdTemplate.swift +37 -0
  47. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  48. package/ios/CarPlay/CarPlayController.swift +105 -67
  49. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  50. package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
  51. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  52. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  53. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  54. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  55. package/ios/Extension/Section+Copying.swift +31 -0
  56. package/ios/Extension/Track+Copying.swift +6 -12
  57. package/ios/Extension/Track+Identity.swift +18 -0
  58. package/ios/HybridAudioBrowser.swift +113 -35
  59. package/ios/Model/NitroTypeStubs.swift +46 -21
  60. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  61. package/ios/Player/NowPlayingUpdater.swift +1 -1
  62. package/ios/Player/PlaybackCoordinator.swift +2 -2
  63. package/ios/Player/RetryManager.swift +4 -3
  64. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  65. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  66. package/ios/Tests/JsonModelsTests.swift +75 -1
  67. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  68. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  69. package/ios/Tests/SearchDrillInTests.swift +16 -16
  70. package/ios/Tests/SectionPresentationTests.swift +61 -0
  71. package/ios/Tests/SectionScopeTests.swift +87 -55
  72. package/ios/Tests/StyleResolverTests.swift +67 -0
  73. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  74. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  75. package/ios/Tests/TrackIdentityTests.swift +20 -0
  76. package/ios/Tests/TrackSelectorTests.swift +168 -33
  77. package/ios/TrackSelector.swift +45 -30
  78. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  79. package/lib/commonjs/AudioBrowser.js +11 -0
  80. package/lib/commonjs/AudioBrowser.js.map +1 -1
  81. package/lib/commonjs/features/browser-config.js +11 -8
  82. package/lib/commonjs/features/browser-config.js.map +1 -1
  83. package/lib/commonjs/features/browser.js +2 -2
  84. package/lib/commonjs/features/favorites.js +13 -10
  85. package/lib/commonjs/features/favorites.js.map +1 -1
  86. package/lib/commonjs/features/playback/controls.js +4 -2
  87. package/lib/commonjs/features/playback/controls.js.map +1 -1
  88. package/lib/commonjs/features/playback/state.js +2 -3
  89. package/lib/commonjs/features/playback/state.js.map +1 -1
  90. package/lib/commonjs/features/player/options.js.map +1 -1
  91. package/lib/commonjs/features/player/setup.js +3 -1
  92. package/lib/commonjs/features/player/setup.js.map +1 -1
  93. package/lib/commonjs/features/player/validateOptions.js +2 -0
  94. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  95. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  96. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  97. package/lib/commonjs/features/queue/queue.js +11 -4
  98. package/lib/commonjs/features/queue/queue.js.map +1 -1
  99. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  100. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  101. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  102. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  103. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  104. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  105. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  106. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  107. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  108. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  109. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  110. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  111. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  112. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  113. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  114. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  115. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  116. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  117. package/lib/commonjs/web/browser/sections.js +89 -0
  118. package/lib/commonjs/web/browser/sections.js.map +1 -0
  119. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  120. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  121. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  122. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  123. package/lib/module/AudioBrowser.js +1 -0
  124. package/lib/module/AudioBrowser.js.map +1 -1
  125. package/lib/module/features/browser-config.js +11 -8
  126. package/lib/module/features/browser-config.js.map +1 -1
  127. package/lib/module/features/browser.js +2 -2
  128. package/lib/module/features/favorites.js +13 -10
  129. package/lib/module/features/favorites.js.map +1 -1
  130. package/lib/module/features/playback/controls.js +4 -2
  131. package/lib/module/features/playback/controls.js.map +1 -1
  132. package/lib/module/features/playback/state.js +2 -3
  133. package/lib/module/features/playback/state.js.map +1 -1
  134. package/lib/module/features/player/options.js.map +1 -1
  135. package/lib/module/features/player/setup.js +3 -1
  136. package/lib/module/features/player/setup.js.map +1 -1
  137. package/lib/module/features/player/validateOptions.js +2 -0
  138. package/lib/module/features/player/validateOptions.js.map +1 -1
  139. package/lib/module/features/queue/activeTrack.js +14 -3
  140. package/lib/module/features/queue/activeTrack.js.map +1 -1
  141. package/lib/module/features/queue/queue.js +11 -4
  142. package/lib/module/features/queue/queue.js.map +1 -1
  143. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  144. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  145. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  146. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  147. package/lib/module/utils/getTrackIdentity.js +20 -0
  148. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  149. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  150. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  151. package/lib/module/web/NativeAudioBrowser.js +32 -24
  152. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  153. package/lib/module/web/browser/BrowserManager.js +144 -87
  154. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  155. package/lib/module/web/browser/FavoriteManager.js +31 -21
  156. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  157. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  158. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  159. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  160. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  161. package/lib/module/web/browser/sections.js +83 -0
  162. package/lib/module/web/browser/sections.js.map +1 -0
  163. package/lib/module/web/player/NowPlayingManager.js +4 -3
  164. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  165. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  166. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  167. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  168. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  169. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  170. package/lib/typescript/src/features/browser.d.ts +2 -2
  171. package/lib/typescript/src/features/favorites.d.ts +13 -10
  172. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  173. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  174. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  175. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  176. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  177. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  178. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  179. package/lib/typescript/src/features/player/options.d.ts +21 -19
  180. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  181. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  182. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  183. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  184. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  185. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  186. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  187. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  188. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  189. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  190. package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
  191. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  192. package/lib/typescript/src/types/browser.d.ts +33 -53
  193. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  194. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  195. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  196. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  197. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  198. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  199. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  200. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  201. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  202. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  203. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  204. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
  205. package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
  206. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  207. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
  208. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
  209. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
  210. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
  211. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
  212. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
  213. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
  214. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
  215. 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
  216. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
  217. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  218. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
  219. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
  220. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
  221. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
  222. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  223. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
  224. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
  225. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
  226. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
  227. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  228. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  229. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  230. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
  231. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  232. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
  233. package/nitrogen/generated/android/c++/JSection.hpp +114 -0
  234. package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
  235. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  236. package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
  237. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
  238. package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
  239. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
  240. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  241. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  242. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  243. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  244. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  245. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  246. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  247. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
  248. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
  249. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  250. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  251. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
  252. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
  253. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
  254. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
  255. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  256. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  257. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  258. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  259. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  260. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  261. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  262. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  263. package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
  264. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  265. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  266. package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
  267. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  268. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  269. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  270. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  271. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  272. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  273. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  274. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  275. package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
  276. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  277. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  278. package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
  279. package/package.json +1 -1
  280. package/src/AudioBrowser.ts +1 -0
  281. package/src/features/browser-config.test.ts +21 -6
  282. package/src/features/browser-config.ts +11 -8
  283. package/src/features/browser.test.ts +1 -1
  284. package/src/features/browser.ts +2 -2
  285. package/src/features/favorites.ts +13 -10
  286. package/src/features/hook-wiring.test.ts +63 -15
  287. package/src/features/playback/controls.ts +4 -2
  288. package/src/features/playback/state.ts +2 -3
  289. package/src/features/playback/trackPlaybackTime.ts +1 -1
  290. package/src/features/player/options.ts +27 -19
  291. package/src/features/player/setup.test.ts +12 -3
  292. package/src/features/player/setup.ts +3 -1
  293. package/src/features/player/validateOptions.ts +2 -0
  294. package/src/features/queue/activeTrack.ts +14 -3
  295. package/src/features/queue/queue.ts +11 -4
  296. package/src/nitro-stub-drift.test.ts +40 -0
  297. package/src/public-surface.test.ts +40 -0
  298. package/src/specs/audio-browser.nitro.ts +4 -3
  299. package/src/style-resolution.test.ts +139 -0
  300. package/src/types/browser-native.ts +2 -2
  301. package/src/types/browser-nodes.ts +268 -136
  302. package/src/types/browser.ts +34 -55
  303. package/src/utils/LazyNativeEmitter.ts +20 -10
  304. package/src/utils/NativeUpdatedValue.ts +29 -13
  305. package/src/utils/getTrackIdentity.ts +20 -0
  306. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  307. package/src/web/NativeAudioBrowser.ts +37 -22
  308. package/src/web/browser/BrowserManager.ts +176 -95
  309. package/src/web/browser/FavoriteManager.test.ts +84 -0
  310. package/src/web/browser/FavoriteManager.ts +28 -23
  311. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  312. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  313. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  314. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  315. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  316. package/src/web/browser/sections.test.ts +91 -0
  317. package/src/web/browser/sections.ts +72 -0
  318. package/src/web/player/NowPlayingManager.ts +4 -3
  319. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  320. package/src/web/util/BrowserPathHelper.ts +61 -13
  321. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  322. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  323. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  324. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  325. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  326. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  327. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  328. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  329. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  330. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  331. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  332. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  333. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -1,94 +1,169 @@
1
1
  package com.audiobrowser.browser
2
2
 
3
- import com.audiobrowser.TestFixtures.imageRowItem
3
+ import com.audiobrowser.TestFixtures.resolvedTrack
4
+ import com.audiobrowser.TestFixtures.section
4
5
  import com.audiobrowser.TestFixtures.track
6
+ import com.margelo.nitro.audiobrowser.SectionStyle
7
+ import com.margelo.nitro.audiobrowser.StyleDisplay
5
8
  import org.junit.Assert.assertEquals
6
9
  import org.junit.Assert.assertNull
7
10
  import org.junit.Test
8
11
 
9
12
  class SectionScopeTest {
10
13
 
11
- @Test
12
- fun `scopes to the contiguous groupTitle run`() {
13
- val children =
14
- listOf(
15
- track(id = "a", src = "a", groupTitle = "First"),
16
- track(id = "b", src = "b", groupTitle = "First"),
17
- track(id = "c", src = "c", groupTitle = "Second"),
18
- track(id = "d", src = "d", groupTitle = "Second"),
19
- track(id = "e", src = "e"),
20
- )
14
+ // Two sections sharing an identity — the duplicate-heavy shape the flat model
15
+ // had to disambiguate by precedence; sections + the stamped flat index make
16
+ // it exact.
17
+ private val sections =
18
+ listOf(
19
+ section(title = "First", children = arrayOf(track(src = "dup"), track(src = "x"))),
20
+ section(
21
+ title = "Second",
22
+ children = arrayOf(track(src = "y"), track(src = "dup"), track(src = "z")),
23
+ ),
24
+ )
21
25
 
22
- val section = SectionScope.section(children, "c") as SectionScope.Section.Run
23
- assertEquals(listOf("c", "d"), section.tracks.map { it.src })
26
+ @Test
27
+ fun `falls back to the first section containing the identity`() {
28
+ val scoped = SectionScope.scoped(sections, "dup")!!
29
+ assertEquals(listOf("dup", "x"), scoped.section.children.map { it.src })
30
+ assertNull(scoped.tappedOffset)
31
+ }
24
32
 
25
- val first = SectionScope.section(children, "a") as SectionScope.Section.Run
26
- assertEquals(listOf("a", "b"), first.tracks.map { it.src })
33
+ @Test
34
+ fun `flat index pins the tapped section`() {
35
+ // Flat index 3 = second section, offset 1.
36
+ val scoped = SectionScope.scoped(sections, "dup", tappedIndex = 3)!!
37
+ assertEquals(listOf("y", "dup", "z"), scoped.section.children.map { it.src })
38
+ assertEquals(1, scoped.tappedOffset)
27
39
  }
28
40
 
29
41
  @Test
30
- fun `ungrouped items form their own run`() {
31
- val children =
42
+ fun `flat index pins the exact copy within a section`() {
43
+ val playlist =
32
44
  listOf(
33
- track(src = "a", groupTitle = "First"),
34
- track(src = "b"),
35
- track(src = "c"),
36
- track(src = "d", groupTitle = "Second"),
45
+ section(
46
+ children = arrayOf(track(src = "a"), track(src = "b"), track(src = "a"), track(src = "c"))
47
+ )
37
48
  )
49
+ val scoped = SectionScope.scoped(playlist, "a", tappedIndex = 2)!!
50
+ assertEquals(2, scoped.tappedOffset)
51
+ }
38
52
 
39
- val section = SectionScope.section(children, "b") as SectionScope.Section.Run
40
- assertEquals(listOf("b", "c"), section.tracks.map { it.src })
53
+ @Test
54
+ fun `stale index falls back to the first identity match`() {
55
+ // The child at the stamped index no longer carries the tapped identity
56
+ // (the list shifted) — resolution ignores the index and pins nothing.
57
+ val scoped = SectionScope.scoped(sections, "dup", tappedIndex = 1)!!
58
+ assertEquals(listOf("dup", "x"), scoped.section.children.map { it.src })
59
+ assertNull(scoped.tappedOffset)
41
60
  }
42
61
 
43
62
  @Test
44
- fun `identical titles in separate runs stay separate`() {
63
+ fun `out-of-range index is ignored`() {
64
+ val scoped = SectionScope.scoped(sections, "dup", tappedIndex = 99)!!
65
+ assertEquals(listOf("dup", "x"), scoped.section.children.map { it.src })
66
+ assertNull(scoped.tappedOffset)
67
+ }
68
+
69
+ @Test
70
+ fun `vanished identity returns null`() {
71
+ assertNull(SectionScope.scoped(sections, "gone"))
72
+ }
73
+
74
+ @Test
75
+ fun `locates a track by its id when it has one`() {
76
+ // Identity rule (ADR 0008): the id wins over the src, so a __trackId stamped
77
+ // from an id-bearing track must resolve — and its src must not.
45
78
  val children =
46
- listOf(
47
- track(src = "a", groupTitle = "Same"),
48
- track(src = "x", groupTitle = "Other"),
49
- track(src = "b", groupTitle = "Same"),
79
+ arrayOf(
80
+ track(id = "stable-a", src = "https://s/a.mp3"),
81
+ track(id = "stable-b", src = "https://s/b.mp3"),
50
82
  )
83
+ val scoped = SectionScope.scoped(listOf(section(children = children)), "stable-b")!!
84
+ assertEquals(listOf("stable-a", "stable-b"), scoped.section.children.map { it.id })
51
85
 
52
- val section = SectionScope.section(children, "a") as SectionScope.Section.Run
53
- assertEquals(listOf("a"), section.tracks.map { it.src })
86
+ // The src is shadowed by the id — it is not the track's identity.
87
+ assertNull(SectionScope.scoped(listOf(section(children = children)), "https://s/b.mp3"))
54
88
  }
55
89
 
90
+ // MARK: page shape helpers
91
+
56
92
  @Test
57
- fun `finds the id inside an image row`() {
58
- val items = arrayOf(imageRowItem("s1"), imageRowItem("s2"))
59
- val row = track(title = "Most Played", src = null, imageRow = items)
60
- val children = listOf(row, track(src = "a"))
93
+ fun `children sugar normalizes to one untitled section`() {
94
+ val page = resolvedTrack(children = arrayOf(track(src = "a"), track(src = "b")))
95
+ val sections = page.normalizedSections!!
96
+ assertEquals(1, sections.size)
97
+ assertNull(sections[0].title)
98
+ assertEquals(listOf("a", "b"), sections[0].children.map { it.src })
99
+ }
100
+
101
+ @Test
102
+ fun `sections win over children`() {
103
+ val page =
104
+ resolvedTrack(
105
+ sections = arrayOf(section(title = "S", children = arrayOf(track(src = "a")))),
106
+ children = arrayOf(track(src = "zzz")),
107
+ )
108
+ val sections = page.normalizedSections!!
109
+ assertEquals(1, sections.size)
110
+ assertEquals("S", sections[0].title)
111
+ }
61
112
 
62
- val section = SectionScope.section(children, "s2") as SectionScope.Section.ImageRow
63
- assertEquals(listOf("s1", "s2"), section.items.map { it.src })
113
+ @Test
114
+ fun `flattened children concatenate in section order`() {
115
+ val page =
116
+ resolvedTrack(
117
+ sections =
118
+ arrayOf(
119
+ section(title = "A", children = arrayOf(track(src = "a1"), track(src = "a2"))),
120
+ section(children = arrayOf(track(src = "b1"))),
121
+ )
122
+ )
123
+ assertEquals(listOf("a1", "a2", "b1"), page.flattenedChildren!!.map { it.src })
64
124
  }
65
125
 
66
126
  @Test
67
- fun `unknown id returns null`() {
68
- assertNull(SectionScope.section(listOf(track(src = "a")), "zz"))
127
+ fun `childless page has no sections`() {
128
+ assertNull(resolvedTrack().normalizedSections)
69
129
  }
70
130
 
71
- // Pins the documented precedence: sections are located by src, and an id
72
- // present in both an image row and the flat list resolves to the row.
131
+ // --- styleResolvedSections: the page->section fold (ADR 0011) ---
132
+
73
133
  @Test
74
- fun `image row wins over a flat-list duplicate`() {
75
- val row = track(title = "Row", src = null, imageRow = arrayOf(imageRowItem("dup")))
76
- val children = listOf(row, track(src = "dup"), track(src = "b"))
134
+ fun `an unstyled section reaches the surface with the page's block folded in`() {
135
+ // The end-to-end wire for ResolvedTrack.style: without this fold, the
136
+ // page tier is a dead field — every existing unit test would stay green
137
+ // while a whole tier of ADR 0011 went dark in production.
138
+ val page =
139
+ resolvedTrack(
140
+ style =
141
+ SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = false),
142
+ sections = arrayOf(section(children = arrayOf(track(src = "a")))),
143
+ )
77
144
 
78
- val section = SectionScope.section(children, "dup") as SectionScope.Section.ImageRow
79
- assertEquals(listOf("dup"), section.items.map { it.src })
145
+ val folded = page.styleResolvedSections()!!.single()
146
+ assertEquals(StyleDisplay.GRID, folded.style?.display)
147
+ assertEquals(false, folded.style?.gridWrap)
148
+ // The extras leg of this wire — folded section → per-item grid hint — is
149
+ // asserted in MediaExtrasBuilderTest (it needs Robolectric for Bundle).
80
150
  }
81
151
 
82
152
  @Test
83
- fun `duplicate src across runs resolves to the earlier run`() {
84
- val children =
85
- listOf(
86
- track(src = "dup", groupTitle = "First"),
87
- track(src = "x", groupTitle = "Second"),
88
- track(src = "dup", groupTitle = "Second"),
153
+ fun `a section's own block overrides the page's in the fold`() {
154
+ val page =
155
+ resolvedTrack(
156
+ style = SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = null),
157
+ sections =
158
+ arrayOf(
159
+ section(
160
+ style =
161
+ SectionStyle(display = StyleDisplay.LIST, artworkRendering = null, gridWrap = null),
162
+ children = arrayOf(track(src = "a")),
163
+ )
164
+ ),
89
165
  )
90
166
 
91
- val section = SectionScope.section(children, "dup") as SectionScope.Section.Run
92
- assertEquals(listOf("dup"), section.tracks.map { it.src })
167
+ assertEquals(StyleDisplay.LIST, page.styleResolvedSections()!!.single().style?.display)
93
168
  }
94
169
  }
@@ -0,0 +1,87 @@
1
+ package com.audiobrowser.browser
2
+
3
+ import com.margelo.nitro.audiobrowser.ArtworkRendering
4
+ import com.margelo.nitro.audiobrowser.SectionStyle
5
+ import com.margelo.nitro.audiobrowser.StyleDisplay
6
+ import com.margelo.nitro.audiobrowser.TrackStyle
7
+ import org.junit.Assert.assertEquals
8
+ import org.junit.Assert.assertNull
9
+ import org.junit.Test
10
+
11
+ /**
12
+ * Resolution semantics (ADR 0011): container properties and `display` resolve by scope override
13
+ * (`section ?? page`), inherited item properties resolve `track ?? section ?? page` — and `display`
14
+ * never inherits onto an item.
15
+ */
16
+ class StyleResolverTest {
17
+
18
+ @Test
19
+ fun `section overrides the page per property`() {
20
+ val resolved =
21
+ StyleResolver.sectionStyle(
22
+ section =
23
+ SectionStyle(display = StyleDisplay.LIST, artworkRendering = null, gridWrap = null),
24
+ page =
25
+ SectionStyle(
26
+ display = StyleDisplay.GRID,
27
+ artworkRendering = ArtworkRendering.STENCIL,
28
+ gridWrap = false,
29
+ ),
30
+ )
31
+ // Declared on the section: wins. Undeclared: the page's scope-wide value.
32
+ assertEquals(StyleDisplay.LIST, resolved.display)
33
+ assertEquals(ArtworkRendering.STENCIL, resolved.artworkRendering)
34
+ assertEquals(false, resolved.gridWrap)
35
+ }
36
+
37
+ @Test
38
+ fun `a styleless section takes the page block`() {
39
+ val resolved =
40
+ StyleResolver.sectionStyle(
41
+ section = null,
42
+ page = SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = true),
43
+ )
44
+ assertEquals(StyleDisplay.GRID, resolved.display)
45
+ assertEquals(true, resolved.gridWrap)
46
+ }
47
+
48
+ @Test
49
+ fun `track overrides the section per property`() {
50
+ val resolved =
51
+ StyleResolver.trackStyle(
52
+ track = TrackStyle(display = null, artworkRendering = ArtworkRendering.ORIGINAL),
53
+ section =
54
+ SectionStyle(
55
+ display = StyleDisplay.GRID,
56
+ artworkRendering = ArtworkRendering.STENCIL,
57
+ gridWrap = null,
58
+ ),
59
+ )
60
+ assertEquals(ArtworkRendering.ORIGINAL, resolved.artworkRendering)
61
+ }
62
+
63
+ @Test
64
+ fun `a styleless track inherits the section value`() {
65
+ val resolved =
66
+ StyleResolver.trackStyle(
67
+ track = null,
68
+ section =
69
+ SectionStyle(display = null, artworkRendering = ArtworkRendering.STENCIL, gridWrap = null),
70
+ )
71
+ assertEquals(ArtworkRendering.STENCIL, resolved.artworkRendering)
72
+ }
73
+
74
+ @Test
75
+ fun `display never inherits onto an item`() {
76
+ // The track's own display is the page promise for the page IT opens; the
77
+ // section's describes its children's layout. Neither may surface as the
78
+ // item's resolved display.
79
+ val resolved =
80
+ StyleResolver.trackStyle(
81
+ track = TrackStyle(display = StyleDisplay.GRID, artworkRendering = null),
82
+ section =
83
+ SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = null),
84
+ )
85
+ assertNull(resolved.display)
86
+ }
87
+ }
@@ -0,0 +1,43 @@
1
+ package com.audiobrowser.extension
2
+
3
+ import com.audiobrowser.TestFixtures.track
4
+ import com.audiobrowser.util.BrowserPathHelper
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Test
7
+ import org.junit.runner.RunWith
8
+ import org.robolectric.RobolectricTestRunner
9
+
10
+ /**
11
+ * The skip-in-place queue match (ADR 0009): exact contextual-path equality pins the tapped copy;
12
+ * the identity match remains for index-less paths; an index-stamped path with no exact match
13
+ * misses, so the caller re-expands. Robolectric for `android.net.Uri` (contextual-path parsing).
14
+ */
15
+ @RunWith(RobolectricTestRunner::class)
16
+ class TappedTrackTest {
17
+
18
+ private fun stamped(id: String, index: Int) = BrowserPathHelper.build("/page", id, index)
19
+
20
+ private val queue =
21
+ arrayOf(
22
+ track(src = "a").copy(path = stamped("a", 0)),
23
+ track(src = "b").copy(path = stamped("b", 1)),
24
+ track(src = "a").copy(path = stamped("a", 2)),
25
+ )
26
+
27
+ @Test
28
+ fun `exact path match pins the tapped copy of a duplicate identity`() {
29
+ assertEquals(2, queue.indexOfTappedTrack(stamped("a", 2), "a"))
30
+ }
31
+
32
+ @Test
33
+ fun `index-less path falls back to the identity match`() {
34
+ assertEquals(0, queue.indexOfTappedTrack(BrowserPathHelper.build("/page", "a"), "a"))
35
+ }
36
+
37
+ @Test
38
+ fun `index-stamped path with no exact match misses instead of identity-skipping`() {
39
+ // A tap in another section whose identity also sits in this queue: the
40
+ // caller must re-expand (re-scoping to the tapped section), not skip.
41
+ assertEquals(-1, queue.indexOfTappedTrack(stamped("a", 7), "a"))
42
+ }
43
+ }
@@ -0,0 +1,41 @@
1
+ package com.audiobrowser.extension
2
+
3
+ import com.audiobrowser.TestFixtures.track
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertNull
6
+ import org.junit.Test
7
+
8
+ /**
9
+ * THE identity rule (ADR 0008): a track's identity is its `id` when non-blank, else its `src`.
10
+ * Everything that compares tracks — favorites, section scoping, skip-in-place, `__trackId` — goes
11
+ * through this extension.
12
+ */
13
+ class TrackIdentityTest {
14
+
15
+ @Test
16
+ fun `id wins over src`() {
17
+ assertEquals("stable-id", track(id = "stable-id", src = "https://s/a.mp3").identity)
18
+ }
19
+
20
+ @Test
21
+ fun `blank id falls back to src`() {
22
+ assertEquals("https://s/a.mp3", track(id = "", src = "https://s/a.mp3").identity)
23
+ assertEquals("https://s/a.mp3", track(id = " ", src = "https://s/a.mp3").identity)
24
+ }
25
+
26
+ @Test
27
+ fun `missing id falls back to src`() {
28
+ assertEquals("https://s/a.mp3", track(id = null, src = "https://s/a.mp3").identity)
29
+ }
30
+
31
+ @Test
32
+ fun `id alone is an identity`() {
33
+ assertEquals("stable-id", track(id = "stable-id", src = null).identity)
34
+ }
35
+
36
+ @Test
37
+ fun `neither id nor src means no identity`() {
38
+ assertNull(track(id = null, src = null).identity)
39
+ assertNull(track(id = "", src = null).identity)
40
+ }
41
+ }
@@ -3,7 +3,6 @@ package com.audiobrowser.player
3
3
  import com.margelo.nitro.audiobrowser.PlayerCapabilities
4
4
  import com.margelo.nitro.audiobrowser.RemoteButton
5
5
  import com.margelo.nitro.audiobrowser.RemoteButtonLayout
6
- import com.margelo.nitro.audiobrowser.Variant_Boolean_FavoriteConfig
7
6
  import org.junit.Assert.assertEquals
8
7
  import org.junit.Assert.assertFalse
9
8
  import org.junit.Assert.assertTrue
@@ -36,7 +35,7 @@ class CapabilityControlsTest {
36
35
  skipToPrevious = skipToPrevious,
37
36
  jumpForward = jumpForward,
38
37
  jumpBackward = jumpBackward,
39
- favorite = favorite?.let { Variant_Boolean_FavoriteConfig.First(it) },
38
+ favorite = favorite,
40
39
  shuffleMode = null,
41
40
  repeatMode = null,
42
41
  playbackRate = null,
@@ -377,7 +377,7 @@ class NowPlayingUpdaterTest {
377
377
  }
378
378
 
379
379
  @Test
380
- fun `artwork re-resolves for a new track id and skips without an id`() = runTest {
380
+ fun `artwork re-resolves for a new track identity including id-less tracks`() = runTest {
381
381
  val surface =
382
382
  FakeSurface().apply {
383
383
  currentTrack = track("A", id = "a")
@@ -393,10 +393,31 @@ class NowPlayingUpdaterTest {
393
393
  runCurrent()
394
394
  assertEquals(2, surface.artworkResolveCount)
395
395
 
396
+ // The config runs for id-less tracks too — no blanket skip.
396
397
  surface.currentTrack = track("C", id = null, src = "https://s/c.mp3")
397
398
  updater.render()
398
399
  runCurrent()
399
- assertEquals(2, surface.artworkResolveCount) // no id → no resolve
400
+ assertEquals(3, surface.artworkResolveCount)
401
+ assertEquals(0 to "https://img/a.png", surface.artworkStamps.last())
402
+ }
403
+
404
+ @Test
405
+ fun `artwork for an id-less track resolves once keyed on src identity`() = runTest {
406
+ val surface =
407
+ FakeSurface().apply {
408
+ currentTrack = track("C", id = null, src = "https://s/c.mp3")
409
+ hasNowPlayingArtworkConfig = true
410
+ artworkResult = "https://img/c.png"
411
+ }
412
+ val updater = NowPlayingUpdater(surface, backgroundScope)
413
+
414
+ updater.render()
415
+ runCurrent()
416
+ updater.updateNowPlaying(update(title = "Song")) // re-render, same track
417
+ runCurrent()
418
+
419
+ assertEquals(1, surface.artworkResolveCount)
420
+ assertEquals(listOf(0 to "https://img/c.png"), surface.artworkStamps)
400
421
  }
401
422
 
402
423
  @Test
@@ -439,4 +460,23 @@ class NowPlayingUpdaterTest {
439
460
 
440
461
  assertTrue(surface.artworkStamps.isEmpty())
441
462
  }
463
+
464
+ @Test
465
+ fun `a stale artwork result for an id-less track is not stamped after a track change`() =
466
+ runTest {
467
+ val surface =
468
+ FakeSurface().apply {
469
+ currentTrack = track("A", id = null, src = "https://s/a.mp3")
470
+ hasNowPlayingArtworkConfig = true
471
+ artworkResult = "https://img/a.png"
472
+ }
473
+ val updater = NowPlayingUpdater(surface, backgroundScope)
474
+
475
+ updater.render() // schedules the artwork resolve
476
+ // Skip to another id-less track before it lands: identity (src) keying must catch it.
477
+ surface.currentTrack = track("B", id = null, src = "https://s/b.mp3")
478
+ runCurrent()
479
+
480
+ assertTrue(surface.artworkStamps.isEmpty())
481
+ }
442
482
  }
@@ -0,0 +1,55 @@
1
+ package com.audiobrowser.player
2
+
3
+ import com.audiobrowser.TestFixtures
4
+ import com.margelo.nitro.audiobrowser.ArtworkRendering
5
+ import com.margelo.nitro.audiobrowser.StyleDisplay
6
+ import com.margelo.nitro.audiobrowser.TrackStyle
7
+ import org.junit.Assert.assertEquals
8
+ import org.junit.Assert.assertNull
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import org.robolectric.RobolectricTestRunner
12
+
13
+ /**
14
+ * The bespoke org.json persistence of the resumption track. ADR 0011: the style block and
15
+ * `disabled` survive process death (a restored queue keeps its visible fidelity), and a snapshot
16
+ * written before the block model — `style` as an enum-name string — restores without a declaration
17
+ * rather than failing.
18
+ */
19
+ @RunWith(RobolectricTestRunner::class)
20
+ class PlaybackStateStoreTrackJsonTest {
21
+
22
+ @Test
23
+ fun `style and disabled survive the round-trip`() {
24
+ val track =
25
+ TestFixtures.track(title = "Night Mix", src = "https://s/a.mp3", disabled = true)
26
+ .copy(
27
+ style =
28
+ TrackStyle(display = StyleDisplay.GRID, artworkRendering = ArtworkRendering.STENCIL)
29
+ )
30
+
31
+ val restored = PlaybackStateStore.trackFromJson(PlaybackStateStore.trackToJson(track))
32
+
33
+ assertEquals(StyleDisplay.GRID, restored?.style?.display)
34
+ assertEquals(ArtworkRendering.STENCIL, restored?.style?.artworkRendering)
35
+ assertEquals(true, restored?.disabled)
36
+ }
37
+
38
+ @Test
39
+ fun `a track without style round-trips without one`() {
40
+ val track = TestFixtures.track(title = "Plain", src = "https://s/a.mp3")
41
+
42
+ val restored = PlaybackStateStore.trackFromJson(PlaybackStateStore.trackToJson(track))
43
+
44
+ assertNull(restored?.style)
45
+ assertNull(restored?.disabled)
46
+ }
47
+
48
+ @Test
49
+ fun `a pre-block snapshot with a string style restores without a declaration`() {
50
+ val restored = PlaybackStateStore.trackFromJson("""{"title":"X","src":"s","style":"LIST"}""")
51
+
52
+ assertEquals("X", restored?.title)
53
+ assertNull(restored?.style)
54
+ }
55
+ }
@@ -1,28 +1,53 @@
1
1
  package com.audiobrowser.util
2
2
 
3
- import org.junit.Assert.assertFalse
4
- import org.junit.Assert.assertTrue
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertNull
5
5
  import org.junit.Test
6
+ import org.junit.runner.RunWith
7
+ import org.robolectric.RobolectricTestRunner
6
8
 
9
+ @RunWith(RobolectricTestRunner::class)
7
10
  class BrowserPathHelperTest {
8
11
 
9
12
  @Test
10
- fun `containsSegment matches a uid as a full path segment`() {
11
- val path = "/listen/amsterdam-funk-channel/Gw0LGB8j"
12
- assertTrue(BrowserPathHelper.containsSegment(path, "Gw0LGB8j"))
13
- assertTrue(BrowserPathHelper.containsSegment(path, "amsterdam-funk-channel"))
14
- assertTrue(BrowserPathHelper.containsSegment(path, "listen"))
15
- assertTrue(BrowserPathHelper.containsSegment("/listen/x/Gw0LGB8j?hl=en", "Gw0LGB8j"))
16
- assertTrue(BrowserPathHelper.containsSegment("/listen/x/Gw0LGB8j#frag", "Gw0LGB8j"))
13
+ fun `build round-trips a src carrying its own query params`() {
14
+ // A src carrying its own query string (signed CDN URL) must survive the
15
+ // build → extract/strip round-trip: an unescaped `&` would split the src
16
+ // into stray query params, truncating the trackId and polluting the
17
+ // parent path.
18
+ val src = "https://cdn.example.com/stream.mp3?token=abc&exp=1699999999"
19
+ val url = BrowserPathHelper.build("/library", src)
20
+ assertEquals(src, BrowserPathHelper.extractTrackId(url))
21
+ assertEquals("/library", BrowserPathHelper.stripTrackId(url))
17
22
  }
18
23
 
19
24
  @Test
20
- fun `containsSegment rejects partial-segment and missing matches`() {
21
- val path = "/listen/amsterdam-funk-channel/Gw0LGB8j"
22
- assertFalse(BrowserPathHelper.containsSegment(path, "Gw0LGB8"))
23
- assertFalse(BrowserPathHelper.containsSegment(path, "funk"))
24
- assertFalse(BrowserPathHelper.containsSegment(path, "msterdam-funk-channe"))
25
- assertFalse(BrowserPathHelper.containsSegment(path, "NopeNope"))
26
- assertFalse(BrowserPathHelper.containsSegment(path, ""))
25
+ fun `build round-trips a src with equals and plus`() {
26
+ val src = "https://cdn.example.com/a+b.mp3?sig=x=y"
27
+ val url = BrowserPathHelper.build("/library", src)
28
+ assertEquals(src, BrowserPathHelper.extractTrackId(url))
29
+ assertEquals("/library", BrowserPathHelper.stripTrackId(url))
30
+ }
31
+
32
+ @Test
33
+ fun `build stamps the index and round-trips it`() {
34
+ val src = "https://cdn.example.com/stream.mp3?token=abc&exp=1"
35
+ val url = BrowserPathHelper.build("/library", src, 3)
36
+ assertEquals(src, BrowserPathHelper.extractTrackId(url))
37
+ assertEquals(3, BrowserPathHelper.extractIndex(url))
38
+ assertEquals("/library", BrowserPathHelper.stripTrackId(url))
39
+ }
40
+
41
+ @Test
42
+ fun `extractIndex returns null without a stamped index`() {
43
+ val url = BrowserPathHelper.build("/library", "song.mp3")
44
+ assertNull(BrowserPathHelper.extractIndex(url))
45
+ }
46
+
47
+ @Test
48
+ fun `extractIndex returns null for non-contextual or malformed-index URLs`() {
49
+ assertNull(BrowserPathHelper.extractIndex("/library"))
50
+ assertNull(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=x"))
51
+ assertNull(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=-1"))
27
52
  }
28
53
  }