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
@@ -2,81 +2,113 @@
2
2
  import Testing
3
3
 
4
4
  struct SectionScopeTests {
5
- private func track(_ src: String, group: String? = nil) -> Track {
6
- Track(id: src, src: src, groupTitle: group)
5
+ private func track(_ src: String, id: String? = nil) -> Track {
6
+ Track(id: id ?? src, src: src)
7
7
  }
8
8
 
9
- private func runSrcs(_ section: SectionScope.Section?) -> [String?]? {
10
- guard case let .run(tracks) = section else { return nil }
11
- return tracks.map(\.src)
9
+ private func section(_ title: String? = nil, _ tracks: [Track]) -> Section {
10
+ Section(title: title, children: tracks)
12
11
  }
13
12
 
14
- private func imageRowSrcs(_ section: SectionScope.Section?) -> [String?]? {
15
- guard case let .imageRow(items) = section else { return nil }
16
- return items.map(\.src)
13
+ private func srcs(_ scoped: SectionScope.Scoped?) -> [String?]? {
14
+ scoped.map { $0.section.children.map(\.src) }
17
15
  }
18
16
 
19
- @Test func scopesToTheContiguousGroupTitleRun() {
20
- let children = [
21
- track("a", group: "First"),
22
- track("b", group: "First"),
23
- track("c", group: "Second"),
24
- track("d", group: "Second"),
25
- track("e"),
17
+ // Two sections sharing an identity — the duplicate-heavy shape the flat
18
+ // model had to disambiguate by precedence; sections + the stamped flat
19
+ // index make it exact.
20
+ private var sections: [Section] {
21
+ [
22
+ section("First", [track("dup"), track("x")]),
23
+ section("Second", [track("y"), track("dup"), track("z")]),
26
24
  ]
27
- #expect(runSrcs(SectionScope.section(of: children, containing: "c")) == ["c", "d"])
28
- #expect(runSrcs(SectionScope.section(of: children, containing: "a")) == ["a", "b"])
29
25
  }
30
26
 
31
- @Test func ungroupedItemsFormTheirOwnRun() {
32
- let children = [
33
- track("a", group: "First"),
34
- track("b"),
35
- track("c"),
36
- track("d", group: "Second"),
37
- ]
38
- #expect(runSrcs(SectionScope.section(of: children, containing: "b")) == ["b", "c"])
27
+ @Test func fallsBackToTheFirstSectionContainingTheIdentity() {
28
+ let scoped = SectionScope.scoped(in: sections, containing: "dup")
29
+ #expect(srcs(scoped) == ["dup", "x"])
30
+ #expect(scoped?.tappedOffset == nil)
39
31
  }
40
32
 
41
- @Test func identicalTitlesInSeparateRunsStaySeparate() {
42
- let children = [
43
- track("a", group: "Same"),
44
- track("x", group: "Other"),
45
- track("b", group: "Same"),
46
- ]
47
- #expect(runSrcs(SectionScope.section(of: children, containing: "a")) == ["a"])
33
+ @Test func flatIndexPinsTheTappedSection() {
34
+ // Flat index 3 = second section, offset 1.
35
+ let scoped = SectionScope.scoped(in: sections, containing: "dup", tappedIndex: 3)
36
+ #expect(srcs(scoped) == ["y", "dup", "z"])
37
+ #expect(scoped?.tappedOffset == 1)
48
38
  }
49
39
 
50
- @Test func findsTheIdInsideAnImageRow() {
51
- let items = [
52
- ImageRowItem(src: "s1", title: "One"),
53
- ImageRowItem(src: "s2", title: "Two"),
54
- ]
55
- var row = Track(id: "row", title: "Most Played")
56
- row.imageRow = items
57
- let children = [row, track("a")]
58
- #expect(imageRowSrcs(SectionScope.section(of: children, containing: "s2")) == ["s1", "s2"])
40
+ @Test func flatIndexPinsTheExactCopyWithinASection() {
41
+ let playlist = [section(nil, [track("a"), track("b"), track("a"), track("c")])]
42
+ let scoped = SectionScope.scoped(in: playlist, containing: "a", tappedIndex: 2)
43
+ #expect(scoped?.tappedOffset == 2)
44
+ }
45
+
46
+ @Test func staleIndexFallsBackToTheFirstIdentityMatch() {
47
+ // The child at the stamped index no longer carries the tapped identity
48
+ // (the list shifted) — resolution ignores the index and pins nothing.
49
+ let scoped = SectionScope.scoped(in: sections, containing: "dup", tappedIndex: 1)
50
+ #expect(srcs(scoped) == ["dup", "x"])
51
+ #expect(scoped?.tappedOffset == nil)
59
52
  }
60
53
 
61
- @Test func unknownIdReturnsNil() {
62
- #expect(SectionScope.section(of: [track("a")], containing: "zz") == nil)
54
+ @Test func outOfRangeIndexIsIgnored() {
55
+ let scoped = SectionScope.scoped(in: sections, containing: "dup", tappedIndex: 99)
56
+ #expect(srcs(scoped) == ["dup", "x"])
57
+ #expect(scoped?.tappedOffset == nil)
63
58
  }
64
59
 
65
- // Pins the documented precedence: sections are located by src, and an id
66
- // present in both an image row and the flat list resolves to the row.
67
- @Test func imageRowWinsOverAFlatListDuplicate() {
68
- var row = Track(id: "row", title: "Row")
69
- row.imageRow = [ImageRowItem(src: "dup", title: "Dup")]
70
- let children = [row, track("dup"), track("b")]
71
- #expect(imageRowSrcs(SectionScope.section(of: children, containing: "dup")) == ["dup"])
60
+ @Test func vanishedIdentityReturnsNil() {
61
+ #expect(SectionScope.scoped(in: sections, containing: "gone") == nil)
72
62
  }
73
63
 
74
- @Test func duplicateSrcAcrossRunsResolvesToTheEarlierRun() {
64
+ // MARK: - Identity (id when non-blank, else src)
65
+
66
+ @Test func matchesChildByIdWhenSrcDiffers() {
75
67
  let children = [
76
- track("dup", group: "First"),
77
- track("x", group: "Second"),
78
- track("dup", group: "Second"),
68
+ Track(id: "stable-a", src: "https://cdn.example/a?token=1"),
69
+ Track(id: "stable-b", src: "https://cdn.example/b?token=2"),
79
70
  ]
80
- #expect(runSrcs(SectionScope.section(of: children, containing: "dup")) == ["dup"])
71
+ let scoped = SectionScope.scoped(in: [section(nil, children)], containing: "stable-b")
72
+ #expect(srcs(scoped) == ["https://cdn.example/a?token=1", "https://cdn.example/b?token=2"])
73
+ }
74
+
75
+ @Test func blankIdFallsBackToSrc() {
76
+ let children = [Track(id: "", src: "s1"), Track(id: "", src: "s2")]
77
+ let scoped = SectionScope.scoped(in: [section(nil, children)], containing: "s2")
78
+ #expect(srcs(scoped) == ["s1", "s2"])
79
+ }
80
+
81
+ // MARK: - Page shape helpers
82
+
83
+ @Test func childrenSugarNormalizesToOneUntitledSection() {
84
+ let page = ResolvedTrack(path: "/p", children: [track("a"), track("b")], title: "P")
85
+ let sections = page.normalizedSections
86
+ #expect(sections?.count == 1)
87
+ #expect(sections?.first?.title == nil)
88
+ #expect(sections?.first?.children.map(\.src) == ["a", "b"])
89
+ }
90
+
91
+ @Test func sectionsWinOverChildren() {
92
+ let page = ResolvedTrack(
93
+ path: "/p",
94
+ sections: [section("S", [track("a")])],
95
+ children: [track("zzz")],
96
+ title: "P",
97
+ )
98
+ #expect(page.normalizedSections?.count == 1)
99
+ #expect(page.normalizedSections?.first?.title == "S")
100
+ }
101
+
102
+ @Test func flattenedChildrenConcatenateInSectionOrder() {
103
+ let page = ResolvedTrack(
104
+ path: "/p",
105
+ sections: [section("A", [track("a1"), track("a2")]), section(nil, [track("b1")])],
106
+ title: "P",
107
+ )
108
+ #expect(page.flattenedChildren?.map(\.src) == ["a1", "a2", "b1"])
109
+ }
110
+
111
+ @Test func childlessPageHasNoSections() {
112
+ #expect(ResolvedTrack(path: "/p", title: "P").normalizedSections == nil)
81
113
  }
82
114
  }
@@ -0,0 +1,67 @@
1
+ import Testing
2
+
3
+ @testable import AudioBrowserTestable
4
+
5
+ // Resolution semantics (ADR 0011): container properties and `display` resolve
6
+ // by scope override (`section ?? page`), inherited item properties resolve
7
+ // `track ?? section ?? page` — and `display` never inherits onto an item.
8
+
9
+ @Suite("StyleResolver.sectionStyle")
10
+ struct SectionStyleResolutionTests {
11
+ @Test func sectionOverridesThePagePerProperty() {
12
+ let resolved = StyleResolver.sectionStyle(
13
+ section: SectionStyle(gridWrap: nil, display: .list, artworkRendering: nil),
14
+ page: SectionStyle(gridWrap: false, display: .grid, artworkRendering: .stencil),
15
+ )
16
+ // Declared on the section: wins. Undeclared: the page's scope-wide value.
17
+ #expect(resolved.display == .list)
18
+ #expect(resolved.artworkRendering == .stencil)
19
+ #expect(resolved.gridWrap == false)
20
+ }
21
+
22
+ @Test func aStylelessSectionTakesThePageBlock() {
23
+ let resolved = StyleResolver.sectionStyle(
24
+ section: nil,
25
+ page: SectionStyle(gridWrap: true, display: .grid, artworkRendering: nil),
26
+ )
27
+ #expect(resolved.display == .grid)
28
+ #expect(resolved.gridWrap == true)
29
+ }
30
+
31
+ @Test func noDeclarationsResolveToAnEmptyBlock() {
32
+ let resolved = StyleResolver.sectionStyle(section: nil, page: nil)
33
+ #expect(resolved.display == nil)
34
+ #expect(resolved.artworkRendering == nil)
35
+ #expect(resolved.gridWrap == nil)
36
+ }
37
+ }
38
+
39
+ @Suite("StyleResolver.trackStyle")
40
+ struct TrackStyleResolutionTests {
41
+ @Test func trackOverridesTheSectionPerProperty() {
42
+ let resolved = StyleResolver.trackStyle(
43
+ track: TrackStyle(display: nil, artworkRendering: .original),
44
+ section: SectionStyle(gridWrap: nil, display: .grid, artworkRendering: .stencil),
45
+ )
46
+ #expect(resolved.artworkRendering == .original)
47
+ }
48
+
49
+ @Test func aStylelessTrackInheritsTheSectionValue() {
50
+ let resolved = StyleResolver.trackStyle(
51
+ track: nil,
52
+ section: SectionStyle(gridWrap: nil, display: nil, artworkRendering: .stencil),
53
+ )
54
+ #expect(resolved.artworkRendering == .stencil)
55
+ }
56
+
57
+ @Test func displayNeverInheritsOntoAnItem() {
58
+ // The track's own `display` is the page promise for the page IT opens;
59
+ // the section's describes its children's layout. Neither may surface as
60
+ // the item's resolved display.
61
+ let resolved = StyleResolver.trackStyle(
62
+ track: TrackStyle(display: .grid, artworkRendering: nil),
63
+ section: SectionStyle(gridWrap: nil, display: .grid, artworkRendering: nil),
64
+ )
65
+ #expect(resolved.display == nil)
66
+ }
67
+ }
@@ -4,59 +4,59 @@ import Testing
4
4
 
5
5
  struct TabBarEntriesTests {
6
6
  private func tab(
7
- _ title: String, url: String, artwork: String? = nil,
7
+ _ title: String, path: String, artwork: String? = nil,
8
8
  artworkSource: ImageSource? = nil, artist: String? = nil,
9
9
  ) -> Track {
10
10
  Track(
11
- id: url, url: url, title: title, artist: artist, artwork: artwork.map { .first($0) },
12
- artworkSource: artworkSource,
11
+ id: path, path: path, artwork: artwork.map { .first($0) },
12
+ artworkSource: artworkSource, title: title, artist: artist,
13
13
  )
14
14
  }
15
15
 
16
16
  @Test func identicalListsAreSame() {
17
- let tabs = [tab("Explore", url: "/explore"), tab("Browse", url: "/browse")]
17
+ let tabs = [tab("Explore", path: "/explore"), tab("Browse", path: "/browse")]
18
18
  #expect(TabBarEntries.same(tabs, tabs))
19
19
  }
20
20
 
21
21
  @Test func titleChangeDiffers() {
22
22
  #expect(
23
23
  !TabBarEntries.same(
24
- [tab("Explore", url: "/explore")],
25
- [tab("Verkennen", url: "/explore")],
24
+ [tab("Explore", path: "/explore")],
25
+ [tab("Verkennen", path: "/explore")],
26
26
  ))
27
27
  }
28
28
 
29
- @Test func urlChangeDiffers() {
29
+ @Test func pathChangeDiffers() {
30
30
  #expect(
31
31
  !TabBarEntries.same(
32
- [tab("Explore", url: "/explore")],
33
- [tab("Explore", url: "/discover")],
32
+ [tab("Explore", path: "/explore")],
33
+ [tab("Explore", path: "/discover")],
34
34
  ))
35
35
  }
36
36
 
37
37
  @Test func artworkChangeDiffers() {
38
38
  #expect(
39
39
  !TabBarEntries.same(
40
- [tab("Explore", url: "/explore", artwork: "sf:globe")],
41
- [tab("Explore", url: "/explore", artwork: "sf:map")],
40
+ [tab("Explore", path: "/explore", artwork: "sf:globe")],
41
+ [tab("Explore", path: "/explore", artwork: "sf:map")],
42
42
  ))
43
43
  }
44
44
 
45
45
  @Test func artworkSourceUriChangeDiffers() {
46
46
  #expect(
47
47
  !TabBarEntries.same(
48
- [tab("Explore", url: "/explore", artworkSource: ImageSource(uri: "https://a/1.png"))],
49
- [tab("Explore", url: "/explore", artworkSource: ImageSource(uri: "https://a/2.png"))],
48
+ [tab("Explore", path: "/explore", artworkSource: ImageSource(uri: "https://a/1.png"))],
49
+ [tab("Explore", path: "/explore", artworkSource: ImageSource(uri: "https://a/2.png"))],
50
50
  ))
51
51
  }
52
52
 
53
53
  @Test func countChangeDiffers() {
54
- let tabs = [tab("Explore", url: "/explore")]
55
- #expect(!TabBarEntries.same(tabs, tabs + [tab("Browse", url: "/browse")]))
54
+ let tabs = [tab("Explore", path: "/explore")]
55
+ #expect(!TabBarEntries.same(tabs, tabs + [tab("Browse", path: "/browse")]))
56
56
  }
57
57
 
58
58
  @Test func nilOldDiffers() {
59
- #expect(!TabBarEntries.same(nil, [tab("Explore", url: "/explore")]))
59
+ #expect(!TabBarEntries.same(nil, [tab("Explore", path: "/explore")]))
60
60
  }
61
61
 
62
62
  @Test func nonRenderedFieldChangeIsSame() {
@@ -64,8 +64,8 @@ struct TabBarEntriesTests {
64
64
  // drift without churning the tab bar.
65
65
  #expect(
66
66
  TabBarEntries.same(
67
- [tab("Explore", url: "/explore", artist: "a")],
68
- [tab("Explore", url: "/explore", artist: "b")],
67
+ [tab("Explore", path: "/explore", artist: "a")],
68
+ [tab("Explore", path: "/explore", artist: "b")],
69
69
  ))
70
70
  }
71
71
  }
@@ -0,0 +1,67 @@
1
+ @testable import AudioBrowserTestable
2
+ import Testing
3
+
4
+ /// `{id}` template substitution used by artwork URL resolution
5
+ /// (BrowserManager+URLResolution.substituteTrackId). A track without a non-blank id must leave
6
+ /// the token LITERALLY in place — never substitute an empty string, never skip — so the
7
+ /// resulting request 404s with a self-describing URL (the resolver logs a warning keyed off
8
+ /// `containsToken`). Mirrors Android's `substituteTrackId` / `configContainsIdToken`.
9
+ struct TrackIdTemplateTests {
10
+ // MARK: substitution with an id
11
+
12
+ @Test func substitutesTokenInString() {
13
+ #expect(TrackIdTemplate.substitute("/artwork/{id}", id: "abc") == "/artwork/abc")
14
+ }
15
+
16
+ @Test func substitutesEveryOccurrence() {
17
+ #expect(TrackIdTemplate.substitute("/{id}/art/{id}.png", id: "x") == "/x/art/x.png")
18
+ }
19
+
20
+ @Test func substitutesTokenInDictionaryValues() {
21
+ let subbed = TrackIdTemplate.substitute(["x-track": "{id}", "plain": "v"], id: "abc")
22
+ #expect(subbed == ["x-track": "abc", "plain": "v"])
23
+ }
24
+
25
+ @Test func valueWithoutTokenIsUnchanged() {
26
+ #expect(TrackIdTemplate.substitute("/artwork/static.png", id: "abc") == "/artwork/static.png")
27
+ }
28
+
29
+ // MARK: token left literal without an id
30
+
31
+ @Test func nilIdLeavesTokenLiteral() {
32
+ #expect(TrackIdTemplate.substitute("/artwork/{id}", id: nil) == "/artwork/{id}")
33
+ }
34
+
35
+ @Test func blankIdLeavesTokenLiteral() {
36
+ #expect(TrackIdTemplate.substitute("/artwork/{id}", id: "") == "/artwork/{id}")
37
+ }
38
+
39
+ @Test func nilIdLeavesDictionaryValuesLiteral() {
40
+ #expect(TrackIdTemplate.substitute(["x-track": "{id}"], id: nil) == ["x-track": "{id}"])
41
+ }
42
+
43
+ @Test func nilValuePassesThrough() {
44
+ #expect(TrackIdTemplate.substitute(String?.none, id: "abc") == nil)
45
+ #expect(TrackIdTemplate.substitute([String: String]?.none, id: nil) == nil)
46
+ }
47
+
48
+ // MARK: unfilled-token detection (drives the single warning)
49
+
50
+ @Test func detectsUnfilledTokenInString() {
51
+ #expect(TrackIdTemplate.containsToken("/artwork/{id}"))
52
+ #expect(!TrackIdTemplate.containsToken("/artwork/abc"))
53
+ #expect(!TrackIdTemplate.containsToken(String?.none))
54
+ }
55
+
56
+ @Test func detectsUnfilledTokenInDictionaryValues() {
57
+ #expect(TrackIdTemplate.containsToken(["x-track": "{id}"]))
58
+ #expect(!TrackIdTemplate.containsToken(["x-track": "abc"]))
59
+ #expect(!TrackIdTemplate.containsToken([String: String]?.none))
60
+ }
61
+
62
+ @Test func substitutionThenDetectionNeverBothMatch() {
63
+ // With an id, substitution clears the token, so no warning fires.
64
+ let subbed = TrackIdTemplate.substitute("/artwork/{id}", id: "abc")
65
+ #expect(!TrackIdTemplate.containsToken(subbed))
66
+ }
67
+ }
@@ -0,0 +1,20 @@
1
+ @testable import AudioBrowserTestable
2
+ import Testing
3
+
4
+ struct TrackIdentityTests {
5
+ @Test func idWinsOverSrc() {
6
+ #expect(Track(id: "stable", src: "https://cdn.example/stream").identity == "stable")
7
+ }
8
+
9
+ @Test func blankIdFallsBackToSrc() {
10
+ #expect(Track(id: "", src: "https://cdn.example/stream").identity == "https://cdn.example/stream")
11
+ }
12
+
13
+ @Test func nilIdFallsBackToSrc() {
14
+ #expect(Track(src: "https://cdn.example/stream").identity == "https://cdn.example/stream")
15
+ }
16
+
17
+ @Test func neitherIdNorSrcHasNoIdentity() {
18
+ #expect(Track(path: "/browse/only").identity == nil)
19
+ }
20
+ }