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

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 (290) hide show
  1. package/README.md +22 -18
  2. package/android/build.gradle +13 -10
  3. package/android/gradle.properties +2 -2
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +76 -21
  6. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  8. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
  9. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +1 -2
  10. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +1 -2
  11. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +6 -7
  12. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  13. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +11 -11
  14. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  15. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +9 -5
  16. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  17. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +0 -1
  18. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -4
  19. package/android/src/main/java/com/audiobrowser/player/Player.kt +30 -8
  20. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  21. package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +2 -1
  22. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
  23. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
  24. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  25. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
  26. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
  27. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  28. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -7
  29. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
  30. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +52 -1
  31. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
  32. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +167 -8
  33. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +2 -1
  34. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +2 -4
  35. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +100 -56
  36. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  37. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  38. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  39. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
  40. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
  41. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  42. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
  43. package/ios/Browser/BrowserManager.swift +2 -27
  44. package/ios/Browser/SearchDrillIn.swift +1 -1
  45. package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
  46. package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
  47. package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
  48. package/ios/HybridAudioBrowser.swift +87 -56
  49. package/ios/Model/NitroTypeStubs.swift +18 -2
  50. package/ios/Model/RemoteCommand.swift +18 -0
  51. package/ios/Model/TrackPlayerError.swift +2 -2
  52. package/ios/Option/SessionCategories.swift +3 -2
  53. package/ios/Player/MediaIntentCriteria.swift +4 -4
  54. package/ios/Player/MediaLoader.swift +3 -1
  55. package/ios/Player/PlaybackCoordinator.swift +4 -1
  56. package/ios/Player/PlaybackErrorHandler.swift +4 -4
  57. package/ios/Player/QueueManager.swift +10 -3
  58. package/ios/Player/RetryManager.swift +79 -18
  59. package/ios/Player/ShuffleOrder.swift +10 -0
  60. package/ios/Player/SleepTimerManager.swift +43 -15
  61. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
  62. package/ios/RemoteCommand/RemoteCommandController.swift +9 -6
  63. package/ios/Tests/LRUCacheTests.swift +111 -0
  64. package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
  65. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  66. package/ios/Tests/MediaLoaderTests.swift +10 -13
  67. package/ios/Tests/PlaybackCoordinatorTests.swift +10 -7
  68. package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
  69. package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
  70. package/ios/Tests/QueueManagerTests.swift +50 -4
  71. package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
  72. package/ios/Tests/RemoteCommandTests.swift +67 -0
  73. package/ios/Tests/RetryManagerTests.swift +89 -0
  74. package/ios/Tests/SectionScopeTests.swift +2 -2
  75. package/ios/Tests/SleepTimerManagerTests.swift +22 -19
  76. package/ios/Tests/Support/ManualSleepTimerScheduler.swift +47 -0
  77. package/ios/Tests/Support/TestTypes.swift +1 -1
  78. package/ios/Tests/TabBarEntriesTests.swift +7 -7
  79. package/ios/TrackPlayer.swift +6 -1
  80. package/ios/TrackPlayerCallbacks.swift +5 -38
  81. package/ios/Util/LRUCache.swift +4 -1
  82. package/lib/commonjs/AudioBrowser.js +0 -11
  83. package/lib/commonjs/AudioBrowser.js.map +1 -1
  84. package/lib/commonjs/CLAUDE.md +52 -0
  85. package/lib/commonjs/features/browser.js +1 -1
  86. package/lib/commonjs/features/browser.js.map +1 -1
  87. package/lib/commonjs/features/carConnection.js +1 -1
  88. package/lib/commonjs/features/equalizer.js +1 -1
  89. package/lib/commonjs/features/errors.js +3 -3
  90. package/lib/commonjs/features/favorites.js +1 -1
  91. package/lib/commonjs/features/gate.js +16 -1
  92. package/lib/commonjs/features/gate.js.map +1 -1
  93. package/lib/commonjs/features/metadata.js +20 -3
  94. package/lib/commonjs/features/metadata.js.map +1 -1
  95. package/lib/commonjs/features/network.js +1 -1
  96. package/lib/commonjs/features/nowPlaying.js +1 -1
  97. package/lib/commonjs/features/output.js +1 -1
  98. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  99. package/lib/commonjs/features/playback/playing.js +1 -1
  100. package/lib/commonjs/features/playback/progress.js +1 -1
  101. package/lib/commonjs/features/playback/state.js +1 -1
  102. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  103. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  104. package/lib/commonjs/features/playback/volume.js +1 -1
  105. package/lib/commonjs/features/player/options.js +48 -34
  106. package/lib/commonjs/features/player/options.js.map +1 -1
  107. package/lib/commonjs/features/player/setup.js +15 -8
  108. package/lib/commonjs/features/player/setup.js.map +1 -1
  109. package/lib/commonjs/features/player/validateOptions.js +21 -0
  110. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  111. package/lib/commonjs/features/queue/activeTrack.js +1 -1
  112. package/lib/commonjs/features/queue/queue.js +2 -2
  113. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  114. package/lib/commonjs/features/queue/shuffle.js +1 -1
  115. package/lib/commonjs/features/remoteControls.js +13 -92
  116. package/lib/commonjs/features/remoteControls.js.map +1 -1
  117. package/lib/commonjs/features/sleepTimer.js +13 -1
  118. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  119. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  120. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  121. package/lib/commonjs/utils/LazyNativeEmitter.js +8 -5
  122. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  123. package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
  124. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  125. package/lib/commonjs/web/browser/BrowserManager.js +1 -1
  126. package/lib/module/AudioBrowser.js +1 -1
  127. package/lib/module/AudioBrowser.js.map +1 -1
  128. package/lib/module/CLAUDE.md +52 -0
  129. package/lib/module/features/browser.js +1 -1
  130. package/lib/module/features/browser.js.map +1 -1
  131. package/lib/module/features/carConnection.js +1 -1
  132. package/lib/module/features/equalizer.js +1 -1
  133. package/lib/module/features/errors.js +3 -3
  134. package/lib/module/features/favorites.js +1 -1
  135. package/lib/module/features/gate.js +16 -1
  136. package/lib/module/features/gate.js.map +1 -1
  137. package/lib/module/features/metadata.js +20 -3
  138. package/lib/module/features/metadata.js.map +1 -1
  139. package/lib/module/features/network.js +1 -1
  140. package/lib/module/features/nowPlaying.js +1 -1
  141. package/lib/module/features/output.js +1 -1
  142. package/lib/module/features/playback/playWhenReady.js +1 -1
  143. package/lib/module/features/playback/playing.js +1 -1
  144. package/lib/module/features/playback/progress.js +1 -1
  145. package/lib/module/features/playback/state.js +1 -1
  146. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  147. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  148. package/lib/module/features/playback/volume.js +1 -1
  149. package/lib/module/features/player/options.js +47 -32
  150. package/lib/module/features/player/options.js.map +1 -1
  151. package/lib/module/features/player/setup.js +14 -7
  152. package/lib/module/features/player/setup.js.map +1 -1
  153. package/lib/module/features/player/validateOptions.js +17 -0
  154. package/lib/module/features/player/validateOptions.js.map +1 -0
  155. package/lib/module/features/queue/activeTrack.js +1 -1
  156. package/lib/module/features/queue/queue.js +2 -2
  157. package/lib/module/features/queue/repeatMode.js +1 -1
  158. package/lib/module/features/queue/shuffle.js +1 -1
  159. package/lib/module/features/remoteControls.js +13 -92
  160. package/lib/module/features/remoteControls.js.map +1 -1
  161. package/lib/module/features/sleepTimer.js +13 -1
  162. package/lib/module/features/sleepTimer.js.map +1 -1
  163. package/lib/module/test-utils/reactNativeStub.js +58 -0
  164. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  165. package/lib/module/utils/LazyNativeEmitter.js +8 -5
  166. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  167. package/lib/module/web/NativeAudioBrowser.js +1 -1
  168. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  169. package/lib/module/web/browser/BrowserManager.js +1 -1
  170. package/lib/typescript/src/AudioBrowser.d.ts +0 -1
  171. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  172. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  173. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  174. package/lib/typescript/src/features/errors.d.ts +3 -3
  175. package/lib/typescript/src/features/favorites.d.ts +1 -1
  176. package/lib/typescript/src/features/gate.d.ts +18 -2
  177. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  178. package/lib/typescript/src/features/metadata.d.ts +20 -3
  179. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  180. package/lib/typescript/src/features/network.d.ts +1 -1
  181. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  182. package/lib/typescript/src/features/output.d.ts +1 -1
  183. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  184. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  185. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  186. package/lib/typescript/src/features/playback/state.d.ts +1 -1
  187. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  188. package/lib/typescript/src/features/player/options.d.ts +87 -37
  189. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  190. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  191. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  192. package/lib/typescript/src/features/player/validateOptions.d.ts +9 -0
  193. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  194. package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
  195. package/lib/typescript/src/features/queue/queue.d.ts +2 -2
  196. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  197. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  198. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  199. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  200. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  201. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  202. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  203. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  204. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -4
  205. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  206. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -1
  207. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
  208. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +5 -5
  209. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +11 -11
  210. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
  211. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +5 -5
  212. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +5 -5
  213. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
  214. package/nitrogen/generated/android/c++/JOptions.hpp +5 -5
  215. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  216. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  217. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  218. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  219. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
  220. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
  221. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
  222. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  223. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
  224. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
  225. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +36 -51
  226. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +6 -6
  227. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +8 -8
  228. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  229. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  230. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  231. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  232. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  233. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  234. package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
  235. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  236. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  237. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  238. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  239. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  240. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  241. package/package.json +12 -4
  242. package/src/AudioBrowser.ts +1 -1
  243. package/src/CLAUDE.md +52 -0
  244. package/src/features/battery.test.ts +79 -0
  245. package/src/features/browser.ts +1 -1
  246. package/src/features/carConnection.ts +1 -1
  247. package/src/features/equalizer.ts +1 -1
  248. package/src/features/errors.ts +3 -3
  249. package/src/features/favorites.ts +1 -1
  250. package/src/features/gate.ts +16 -1
  251. package/src/features/hook-wiring.test.ts +254 -0
  252. package/src/features/metadata.ts +20 -3
  253. package/src/features/network.ts +1 -1
  254. package/src/features/nowPlaying.ts +1 -1
  255. package/src/features/output.ts +1 -1
  256. package/src/features/playback/playWhenReady.ts +1 -1
  257. package/src/features/playback/playing.ts +1 -1
  258. package/src/features/playback/progress.test.ts +169 -0
  259. package/src/features/playback/progress.ts +1 -1
  260. package/src/features/playback/state.ts +1 -1
  261. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  262. package/src/features/playback/trackPlaybackTime.ts +0 -5
  263. package/src/features/playback/volume.ts +1 -1
  264. package/src/features/player/options.ts +99 -57
  265. package/src/features/player/setup.test.ts +12 -4
  266. package/src/features/player/setup.ts +19 -9
  267. package/src/features/player/validateOptions.ts +19 -0
  268. package/src/features/queue/activeTrack.ts +1 -1
  269. package/src/features/queue/queue.ts +2 -2
  270. package/src/features/queue/repeatMode.ts +1 -1
  271. package/src/features/queue/shuffle.ts +1 -1
  272. package/src/features/remoteControls.ts +13 -92
  273. package/src/features/sleepTimer.test.ts +172 -0
  274. package/src/features/sleepTimer.ts +9 -1
  275. package/src/nitro-stub-drift.test.ts +237 -0
  276. package/src/public-surface.test.ts +107 -0
  277. package/src/specs/audio-browser.nitro.ts +1 -1
  278. package/src/test-utils/reactNativeStub.ts +60 -0
  279. package/src/utils/LazyNativeEmitter.ts +9 -6
  280. package/src/utils/useDebug.test.ts +182 -0
  281. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  282. package/src/web/NativeAudioBrowser.ts +1 -1
  283. package/src/web/browser/BrowserManager.ts +1 -1
  284. package/ios/Model/MediaURL.swift +0 -67
  285. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  286. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  287. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  289. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  290. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -0,0 +1,322 @@
1
+ package com.audiobrowser.tls
2
+
3
+ import java.net.InetAddress
4
+ import java.net.ServerSocket
5
+ import java.security.cert.X509Certificate
6
+ import java.util.concurrent.CountDownLatch
7
+ import java.util.concurrent.TimeUnit
8
+ import java.util.concurrent.atomic.AtomicInteger
9
+ import java.util.concurrent.atomic.AtomicReference
10
+ import javax.security.auth.x500.X500Principal
11
+ import kotlin.concurrent.thread
12
+ import org.junit.After
13
+ import org.junit.Assert.assertEquals
14
+ import org.junit.Assert.assertFalse
15
+ import org.junit.Assert.assertTrue
16
+ import org.junit.Test
17
+ import org.mockito.Mockito.mock
18
+ import org.mockito.Mockito.`when`
19
+
20
+ /**
21
+ * Exercises [CachingCertificateFetcher.fetch] against a real loopback server.
22
+ *
23
+ * The safety helpers have their own unit tests, but a predicate that is never consulted protects
24
+ * nothing: these cover the *wiring*, so that removing the `isSafeUrl` guard or swapping
25
+ * `readCapped` back for a plain read-to-EOF fails a test rather than passing silently.
26
+ */
27
+ class CachingCertificateFetcherCallSiteTest {
28
+
29
+ private var server: ServerSocket? = null
30
+
31
+ @After
32
+ fun tearDown() {
33
+ server?.close()
34
+ }
35
+
36
+ /**
37
+ * Serves [response] to every connection, recording the request line of the first. [handled]
38
+ * counts accepted connections.
39
+ */
40
+ private fun startServer(
41
+ response: ByteArray = "HTTP/1.0 404 Not Found\r\n\r\n".toByteArray(),
42
+ handled: AtomicInteger = AtomicInteger(),
43
+ firstRequest: AtomicReference<String> = AtomicReference(),
44
+ ): Int {
45
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
46
+ server = socket
47
+ thread(isDaemon = true) {
48
+ while (!socket.isClosed) {
49
+ try {
50
+ socket.accept().use { client ->
51
+ handled.incrementAndGet()
52
+ val request = ByteArray(1024)
53
+ val read = client.getInputStream().read(request)
54
+ firstRequest.compareAndSet(null, String(request, 0, maxOf(read, 0)))
55
+ client.getOutputStream().apply {
56
+ write(response)
57
+ flush()
58
+ }
59
+ }
60
+ } catch (e: Exception) {
61
+ return@thread // socket closed, or the client hung up
62
+ }
63
+ }
64
+ }
65
+ return socket.localPort
66
+ }
67
+
68
+ @Test
69
+ fun `a URL carrying CRLF never reaches the network`() {
70
+ val handled = AtomicInteger()
71
+ val port = startServer(handled = handled)
72
+
73
+ // Without the isSafeUrl guard this connects and emits "SET foo bar" as its own request line.
74
+ val certs = CachingCertificateFetcher().fetch("http://127.0.0.1:$port/\r\nSET foo bar\r\n")
75
+
76
+ assertTrue(certs.isEmpty())
77
+ assertEquals("the request must not be sent at all", 0, handled.get())
78
+ }
79
+
80
+ @Test
81
+ fun `a well-formed URL does reach the network, so the guard is not just refusing everything`() {
82
+ val handled = AtomicInteger()
83
+ val request = AtomicReference<String>()
84
+ val port = startServer(handled = handled, firstRequest = request)
85
+
86
+ CachingCertificateFetcher().fetch("http://127.0.0.1:$port/issuer.crt")
87
+
88
+ assertEquals(1, handled.get())
89
+ assertTrue("request line was: ${request.get()}", request.get().startsWith("GET /issuer.crt "))
90
+ }
91
+
92
+ @Test
93
+ fun `a non-default port is carried in the Host header`() {
94
+ val request = AtomicReference<String>()
95
+ val port = startServer(firstRequest = request)
96
+
97
+ CachingCertificateFetcher().fetch("http://127.0.0.1:$port/issuer.crt")
98
+
99
+ assertTrue(
100
+ "headers were: ${request.get()}",
101
+ request.get().contains("Host: 127.0.0.1:$port\r\n"),
102
+ )
103
+ }
104
+
105
+ @Test(timeout = 30_000)
106
+ fun `an oversized response is abandoned rather than read to EOF`() {
107
+ // Asserting only that the fetch fails would prove nothing — 4 MiB crosses loopback in
108
+ // milliseconds and would fail to parse either way. What has to be true is that the client
109
+ // stopped reading, which shows up as the server's write not draining.
110
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
111
+ server = socket
112
+ val written = AtomicInteger()
113
+ val finished = CountDownLatch(1)
114
+ thread(isDaemon = true) {
115
+ runCatching {
116
+ socket.accept().use { client ->
117
+ client.getInputStream().read(ByteArray(1024))
118
+ val out = client.getOutputStream()
119
+ out.write("HTTP/1.0 200 OK\r\n\r\n".toByteArray())
120
+ val chunk = ByteArray(64 * 1024)
121
+ // 64 MiB — far past the 1 MiB cap. Once the client stops reading and closes, this
122
+ // throws a broken pipe, which is the signal we want.
123
+ repeat(1024) {
124
+ out.write(chunk)
125
+ written.addAndGet(chunk.size)
126
+ }
127
+ }
128
+ }
129
+ finished.countDown()
130
+ }
131
+
132
+ val certs = CachingCertificateFetcher().fetch("http://127.0.0.1:${socket.localPort}/big.crt")
133
+
134
+ assertTrue(certs.isEmpty())
135
+ assertTrue(finished.await(20, TimeUnit.SECONDS))
136
+ // Socket buffers mean the server gets somewhat past the cap before the write fails, but it
137
+ // must be nowhere near the 64 MiB it wanted to send.
138
+ assertTrue(
139
+ "server wrote ${written.get()} bytes; the read was not cut off",
140
+ written.get() < 16 shl 20,
141
+ )
142
+ }
143
+
144
+ @Test(timeout = 30_000)
145
+ fun `a real certificate is fetched, parsed and then served from cache`() {
146
+ // The success path, end to end over a socket: every other wire test serves a failure, so a
147
+ // broken body offset or a broken cache write would not show up anywhere.
148
+ val der = CertFixtures.bytes("letsencrypt-r13.der")
149
+ val handled = AtomicInteger()
150
+ val port =
151
+ startServer(
152
+ response =
153
+ ("HTTP/1.0 200 OK\r\nContent-Type: application/pkix-cert\r\n\r\n").toByteArray() + der,
154
+ handled = handled,
155
+ )
156
+ val fetcher = CachingCertificateFetcher()
157
+ val url = "http://127.0.0.1:$port/r13.der"
158
+
159
+ val certs = fetcher.fetch(url)
160
+
161
+ assertEquals(1, certs.size)
162
+ assertEquals("CN=R13,O=Let's Encrypt,C=US", certs.first().subjectX500Principal.name)
163
+
164
+ // Second call must be served from the cache, not the network.
165
+ assertEquals(certs, fetcher.fetch(url))
166
+ assertEquals("a successful fetch must be cached", 1, handled.get())
167
+ }
168
+
169
+ /**
170
+ * A mock certificate whose AIA extension lists [url] as its one CA-Issuers location, so
171
+ * [AiaCertChaser.completeChain] chases it with this fetcher — the real wiring, end to end.
172
+ */
173
+ private fun certPointingAt(url: String): X509Certificate {
174
+ // Short-form DER lengths only; a loopback URL is far under 128 bytes.
175
+ fun der(tag: Int, content: ByteArray) =
176
+ byteArrayOf(tag.toByte(), content.size.toByte()) + content
177
+ val caIssuersOid = byteArrayOf(0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02)
178
+ val aia =
179
+ der(
180
+ 0x04,
181
+ der(
182
+ 0x30,
183
+ der(0x30, der(0x06, caIssuersOid) + der(0x86, url.toByteArray(Charsets.US_ASCII))),
184
+ ),
185
+ )
186
+ return mock(X509Certificate::class.java).also {
187
+ `when`(it.getExtensionValue("1.3.6.1.5.5.7.1.1")).thenReturn(aia)
188
+ // Distinct principals, or the chaser reads the mock as a self-signed root and never fetches.
189
+ `when`(it.subjectX500Principal).thenReturn(X500Principal("CN=leaf"))
190
+ `when`(it.issuerX500Principal).thenReturn(X500Principal("CN=issuer"))
191
+ }
192
+ }
193
+
194
+ @Test(timeout = 30_000)
195
+ fun `a drip-feeding server is cut off by the chase deadline, not only the per-fetch caps`() {
196
+ // One byte every 100 ms: each read lands well inside the socket timeout, so per-read limits
197
+ // never fire, and the per-fetch read budget alone would let this run for its full 10 s. The
198
+ // walk's 1 s budget must cut the fetch *itself* — not merely decline to start the next one.
199
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
200
+ server = socket
201
+ thread(isDaemon = true) {
202
+ runCatching {
203
+ socket.accept().use { client ->
204
+ client.getInputStream().read(ByteArray(1024))
205
+ val out = client.getOutputStream()
206
+ out.write("HTTP/1.0 200 OK\r\n\r\n".toByteArray())
207
+ out.flush()
208
+ repeat(300) { // 30 s of drip on offer; the client must not stay for it
209
+ out.write(0)
210
+ out.flush()
211
+ Thread.sleep(100)
212
+ }
213
+ }
214
+ }
215
+ }
216
+ val fetcher = CachingCertificateFetcher()
217
+ val cert = certPointingAt("http://127.0.0.1:${socket.localPort}/drip.crt")
218
+
219
+ val started = System.nanoTime()
220
+ val completed =
221
+ AiaCertChaser.completeChain(listOf(cert), budgetMs = 1_000) { url, deadline ->
222
+ fetcher.fetch(url, deadline)
223
+ }
224
+ val elapsedMs = (System.nanoTime() - started) / 1_000_000
225
+
226
+ assertEquals(listOf(cert), completed)
227
+ // Well past the 1 s budget to absorb scheduling noise, but far short of the 10 s the
228
+ // per-fetch read budget alone would have allowed.
229
+ assertTrue("chase ran for $elapsedMs ms", elapsedMs < 8_000)
230
+ }
231
+
232
+ @Test(timeout = 30_000)
233
+ fun `the chase deadline spans redirect hops, not just fetch starts`() {
234
+ // Every hop stalls 1 s and then redirects back to itself. Each hop sits comfortably inside
235
+ // the per-operation timeouts, so only a deadline threaded through the redirect recursion
236
+ // can end the fetch — without one it walks all 5 redirects, ~6 s here.
237
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
238
+ server = socket
239
+ thread(isDaemon = true) {
240
+ while (!socket.isClosed) {
241
+ try {
242
+ socket.accept().use { client ->
243
+ client.getInputStream().read(ByteArray(1024))
244
+ Thread.sleep(1_000)
245
+ client.getOutputStream().apply {
246
+ write("HTTP/1.0 302 Found\r\nLocation: /again.crt\r\n\r\n".toByteArray())
247
+ flush()
248
+ }
249
+ }
250
+ } catch (e: Exception) {
251
+ return@thread
252
+ }
253
+ }
254
+ }
255
+ val fetcher = CachingCertificateFetcher()
256
+
257
+ val started = System.nanoTime()
258
+ val certs =
259
+ fetcher.fetch(
260
+ "http://127.0.0.1:${socket.localPort}/loop.crt",
261
+ deadlineNanos = System.nanoTime() + 1_500 * 1_000_000L,
262
+ )
263
+ val elapsedMs = (System.nanoTime() - started) / 1_000_000
264
+
265
+ assertTrue(certs.isEmpty())
266
+ assertTrue("fetch ran for $elapsedMs ms across redirect hops", elapsedMs < 4_000)
267
+ }
268
+
269
+ @Test(timeout = 30_000)
270
+ fun `a server that accepts and never answers is bounded by the read timeout`() {
271
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
272
+ server = socket
273
+ val accepted = CountDownLatch(1)
274
+ // Accept, then hold the connection open saying nothing at all.
275
+ thread(isDaemon = true) {
276
+ runCatching {
277
+ val client = socket.accept()
278
+ accepted.countDown()
279
+ Thread.sleep(60_000)
280
+ client.close()
281
+ }
282
+ }
283
+
284
+ val certs =
285
+ CachingCertificateFetcher(connectTimeoutMs = 2_000, readTimeoutMs = 1_000)
286
+ .fetch("http://127.0.0.1:${socket.localPort}/hangs.crt")
287
+
288
+ assertTrue(accepted.await(10, TimeUnit.SECONDS))
289
+ assertTrue(certs.isEmpty())
290
+ }
291
+
292
+ @Test
293
+ fun `a non-http scheme is never fetched`() {
294
+ val handled = AtomicInteger()
295
+ val port = startServer(handled = handled)
296
+
297
+ assertTrue(CachingCertificateFetcher().fetch("ftp://127.0.0.1:$port/issuer.crt").isEmpty())
298
+ assertTrue(CachingCertificateFetcher().fetch("file:///etc/hosts").isEmpty())
299
+ assertEquals(0, handled.get())
300
+ }
301
+
302
+ @Test
303
+ fun `a failed fetch is not cached, so it stays retryable`() {
304
+ val handled = AtomicInteger()
305
+ val port = startServer(handled = handled)
306
+ val fetcher = CachingCertificateFetcher()
307
+ val url = "http://127.0.0.1:$port/issuer.crt"
308
+
309
+ fetcher.fetch(url)
310
+ fetcher.fetch(url)
311
+
312
+ assertEquals("both attempts must hit the network", 2, handled.get())
313
+ }
314
+
315
+ @Test
316
+ fun `the cache is bounded`() {
317
+ // Far more distinct URLs than MAX_CACHE_ENTRIES; nothing is cached here (every fetch fails),
318
+ // but the bound is what stops an attacker-chosen URL space from growing without limit.
319
+ assertFalse(CachingCertificateFetcher.MAX_CACHE_ENTRIES <= 0)
320
+ assertTrue(CachingCertificateFetcher.MAX_CACHE_ENTRIES <= 128)
321
+ }
322
+ }
@@ -1,5 +1,7 @@
1
1
  package com.audiobrowser.tls
2
2
 
3
+ import java.io.ByteArrayInputStream
4
+ import java.io.InputStream
3
5
  import org.junit.Assert.assertArrayEquals
4
6
  import org.junit.Assert.assertEquals
5
7
  import org.junit.Assert.assertNull
@@ -72,4 +74,85 @@ class CachingCertificateFetcherTest {
72
74
  CachingCertificateFetcher.parseHttpResponse("garbage without a header break".toByteArray())
73
75
  )
74
76
  }
77
+
78
+ // -- response cap (the AIA URL, so the host serving it, comes from the presented cert) --
79
+
80
+ // -- URL validation (the AIA URL is attacker-supplied text from the presented cert) --
81
+
82
+ @Test
83
+ fun `accepts the URL form CAs actually serve`() {
84
+ assertTrue(CachingCertificateFetcher.isSafeUrl("http://r13.i.lencr.org/"))
85
+ assertTrue(CachingCertificateFetcher.isSafeUrl("http://ca.example:8080/issuer.crt?v=2"))
86
+ }
87
+
88
+ @Test
89
+ fun `rejects a URL carrying CRLF, which would inject request lines`() {
90
+ // java.net.URL keeps the CRLF in `file`, and the cleartext fetch writes `file` straight
91
+ // into "GET $path HTTP/1.0\r\n" on a raw socket.
92
+ assertTrue(!CachingCertificateFetcher.isSafeUrl("http://127.0.0.1:6379/\r\nSET foo bar\r\n"))
93
+ assertTrue(!CachingCertificateFetcher.isSafeUrl("http://ca.example/a\nX-Injected: 1"))
94
+ }
95
+
96
+ @Test
97
+ fun `rejects a URL carrying a space, which would split the request line`() {
98
+ assertTrue(!CachingCertificateFetcher.isSafeUrl("http://ca.example/a b"))
99
+ }
100
+
101
+ @Test
102
+ fun `reads a response that fits within the cap`() {
103
+ val body = ByteArray(64) { it.toByte() }
104
+
105
+ assertArrayEquals(
106
+ body,
107
+ CachingCertificateFetcher.readCapped(ByteArrayInputStream(body), limit = 128),
108
+ )
109
+ }
110
+
111
+ @Test
112
+ fun `returns null for a response that exceeds the cap`() {
113
+ val body = ByteArray(129)
114
+
115
+ assertNull(CachingCertificateFetcher.readCapped(ByteArrayInputStream(body), limit = 128))
116
+ }
117
+
118
+ @Test
119
+ fun `a stream that never ends does not read without bound`() {
120
+ // Endless: read() always fills the buffer and never signals EOF, the shape a slow-drip
121
+ // server takes — the per-read timeout never fires because every read succeeds.
122
+ val endless =
123
+ object : InputStream() {
124
+ override fun read() = 0
125
+
126
+ override fun read(b: ByteArray, off: Int, len: Int): Int = len
127
+ }
128
+
129
+ assertNull(CachingCertificateFetcher.readCapped(endless, limit = 1 shl 16))
130
+ }
131
+
132
+ @Test
133
+ fun `a slow drip is cut off by the deadline, not left to exhaust the byte cap`() {
134
+ // One byte per read, each one landing just inside the socket's read timeout: the byte cap
135
+ // alone would let this run for a million reads. The clock advances 1s per read, so the
136
+ // 10s budget must end it after ~10 — far short of the 1 MiB cap.
137
+ var reads = 0
138
+ val drip =
139
+ object : InputStream() {
140
+ override fun read() = 0
141
+
142
+ override fun read(b: ByteArray, off: Int, len: Int): Int {
143
+ reads++
144
+ return 1
145
+ }
146
+ }
147
+ var nanos = 0L
148
+
149
+ val result =
150
+ CachingCertificateFetcher.readCapped(drip, budgetMs = 10_000) {
151
+ nanos += 1_000_000_000
152
+ nanos
153
+ }
154
+
155
+ assertNull(result)
156
+ assertTrue("gave up after $reads reads", reads < 20)
157
+ }
75
158
  }
@@ -64,9 +64,6 @@ final class BrowserManager {
64
64
  private let router = SimpleRouter()
65
65
  private let httpClient = HttpClient()
66
66
 
67
- // LRU cache for individual tracks - keyed by both url and src for O(1) lookup
68
- private let trackCache = LRUCache<String, Track>(maxSize: 3000)
69
-
70
67
  // LRU cache for resolved content - keyed by path
71
68
  private let contentCache = LRUCache<String, ResolvedTrack>(maxSize: 20)
72
69
 
@@ -223,22 +220,6 @@ final class BrowserManager {
223
220
  return resolvedTrack.copying(children: hydratedChildren)
224
221
  }
225
222
 
226
- // MARK: - Track Cache
227
-
228
- /// Cache a track by both url and src for O(1) lookup from either key.
229
- private func cacheTrack(_ track: Track) {
230
- if let url = track.url {
231
- trackCache.set(url, value: track)
232
- }
233
- if let src = track.src {
234
- trackCache.set(src, value: track)
235
- }
236
- }
237
-
238
- private func cacheChildren(_ resolvedTrack: ResolvedTrack) {
239
- resolvedTrack.children?.forEach { cacheTrack($0) }
240
- }
241
-
242
223
  // MARK: - Navigation
243
224
 
244
225
  /// Main navigation method - updates path and resolves content.
@@ -276,9 +257,6 @@ final class BrowserManager {
276
257
  // Cache the resolved content for future navigation
277
258
  contentCache.set(normalizedPath, value: resolvedTrack)
278
259
 
279
- // Cache children for track lookups
280
- cacheChildren(resolvedTrack)
281
-
282
260
  return hydrateChildren(resolvedTrack)
283
261
  }
284
262
 
@@ -682,9 +660,6 @@ final class BrowserManager {
682
660
  lastSearchQuery = query
683
661
  lastSearchResults = hydratedResults
684
662
 
685
- // Cache individual tracks
686
- hydratedResults.forEach { cacheTrack($0) }
687
-
688
663
  return makeSearchResult(query: query, results: hydratedResults)
689
664
  }
690
665
 
@@ -770,9 +745,9 @@ final class BrowserManager {
770
745
  // silently queueing the changed list would resume the wrong station.
771
746
  let sectionTracks: [Track]
772
747
  switch SectionScope.section(of: children, containing: trackId) {
773
- case .imageRow(let items):
748
+ case let .imageRow(items):
774
749
  sectionTracks = items.map { $0.toTrack() }
775
- case .run(let tracks):
750
+ case let .run(tracks):
776
751
  sectionTracks = tracks
777
752
  case nil:
778
753
  return nil
@@ -20,7 +20,7 @@ enum SearchDrillIn {
20
20
  /// yields nothing. Returns nil when nothing is playable.
21
21
  static func playable<Item: PlayableSearchItem>(
22
22
  from children: [Item],
23
- resolveChildren: sending (String) async throws -> [Item]
23
+ resolveChildren: sending (String) async throws -> [Item],
24
24
  ) async throws -> [Item]? {
25
25
  guard !children.isEmpty else { return nil }
26
26
 
@@ -22,8 +22,8 @@ class RNABMediaAddHandler: NSObject, INAddMediaIntentHandling {
22
22
  private static let logger = Logger(subsystem: "com.audiobrowser", category: "MediaAddHandler")
23
23
 
24
24
  func handle(
25
- intent: INAddMediaIntent,
26
- completion: @escaping @Sendable (INAddMediaIntentResponse) -> Void
25
+ intent _: INAddMediaIntent,
26
+ completion: @escaping @Sendable (INAddMediaIntentResponse) -> Void,
27
27
  ) {
28
28
  Self.logger.info("Add media — favorite current track")
29
29
 
@@ -57,7 +57,7 @@ class RNABMediaAddHandler: NSObject, INAddMediaIntentHandling {
57
57
  @objc(resolveMediaItemsForAddMedia:withCompletion:)
58
58
  func resolveMediaItems(
59
59
  for intent: INAddMediaIntent,
60
- with completion: @escaping ([INAddMediaMediaItemResolutionResult]) -> Void
60
+ with completion: @escaping ([INAddMediaMediaItemResolutionResult]) -> Void,
61
61
  ) {
62
62
  let name = intent.mediaSearch?.mediaName ?? ""
63
63
  let item = INMediaItem(identifier: name, title: name, type: .unknown, artwork: nil)
@@ -66,10 +66,10 @@ class RNABMediaAddHandler: NSObject, INAddMediaIntentHandling {
66
66
 
67
67
  @objc(resolveMediaDestinationForAddMedia:withCompletion:)
68
68
  func resolveMediaDestination(
69
- for intent: INAddMediaIntent,
70
- with completion: @escaping (INAddMediaMediaDestinationResolutionResult) -> Void
69
+ for _: INAddMediaIntent,
70
+ with completion: @escaping (INAddMediaMediaDestinationResolutionResult) -> Void,
71
71
  ) {
72
72
  // One flat favorites collection — always the user's library, no playlists.
73
- completion(.success(with: INMediaDestination.library))
73
+ completion(.success(with: INMediaDestination.library))
74
74
  }
75
75
  }
@@ -21,7 +21,7 @@ class RNABMediaAffinityHandler: NSObject, INUpdateMediaAffinityIntentHandling {
21
21
 
22
22
  func handle(
23
23
  intent: INUpdateMediaAffinityIntent,
24
- completion: @escaping @Sendable (INUpdateMediaAffinityIntentResponse) -> Void
24
+ completion: @escaping @Sendable (INUpdateMediaAffinityIntentResponse) -> Void,
25
25
  ) {
26
26
  let affinity = intent.affinityType
27
27
  Self.logger.info("Update media affinity — type=\(affinity.rawValue)")
@@ -60,7 +60,7 @@ class RNABMediaAffinityHandler: NSObject, INUpdateMediaAffinityIntentHandling {
60
60
  @objc(resolveMediaItemsForUpdateMediaAffinity:withCompletion:)
61
61
  func resolveMediaItems(
62
62
  for intent: INUpdateMediaAffinityIntent,
63
- with completion: @escaping ([INUpdateMediaAffinityMediaItemResolutionResult]) -> Void
63
+ with completion: @escaping ([INUpdateMediaAffinityMediaItemResolutionResult]) -> Void,
64
64
  ) {
65
65
  let name = intent.mediaSearch?.mediaName ?? ""
66
66
  let item = INMediaItem(identifier: name, title: name, type: .unknown, artwork: nil)
@@ -21,7 +21,7 @@ class RNABMediaIntentHandler: NSObject, INPlayMediaIntentHandling {
21
21
  mediaTypeMode: Self.mediaTypeMode(s?.mediaType ?? .unknown),
22
22
  reference: Self.reference(s?.reference ?? .unknown),
23
23
  hasMediaType: (s?.mediaType ?? .unknown) != .unknown,
24
- appName: Self.hostAppName()
24
+ appName: Self.hostAppName(),
25
25
  )
26
26
  Self.logger.info("Play media intent — query=\(criteria.query) matchesApp=\(criteria.matchesAppName) resume=\(criteria.isResume)")
27
27
 
@@ -37,18 +37,18 @@ class RNABMediaIntentHandler: NSObject, INPlayMediaIntentHandling {
37
37
  /// `album` are filters, not verticals — they yield no mode.
38
38
  private static func mediaTypeMode(_ type: INMediaItemType) -> String? {
39
39
  switch type {
40
- case .station, .radioStation, .algorithmicRadioStation, .musicStation: return "station"
41
- case .podcastShow, .podcastEpisode, .podcastPlaylist, .podcastStation: return "podcast"
42
- case .audioBook: return "audiobook"
43
- case .news: return "news"
44
- case .music: return "music"
45
- case .song: return "song"
46
- case .playlist: return "playlist"
47
- case .musicVideo: return "music-video"
48
- case .movie: return "movie"
49
- case .tvShow: return "tv-show"
50
- case .tvShowEpisode: return "tv-show-episode"
51
- default: return nil // album/artist/genre/unknown
40
+ case .station, .radioStation, .algorithmicRadioStation, .musicStation: "station"
41
+ case .podcastShow, .podcastEpisode, .podcastPlaylist, .podcastStation: "podcast"
42
+ case .audioBook: "audiobook"
43
+ case .news: "news"
44
+ case .music: "music"
45
+ case .song: "song"
46
+ case .playlist: "playlist"
47
+ case .musicVideo: "music-video"
48
+ case .movie: "movie"
49
+ case .tvShow: "tv-show"
50
+ case .tvShowEpisode: "tv-show-episode"
51
+ default: nil // album/artist/genre/unknown
52
52
  }
53
53
  }
54
54
 
@@ -56,9 +56,9 @@ class RNABMediaIntentHandler: NSObject, INPlayMediaIntentHandling {
56
56
  /// routes to native resume; `.my` to the consumer; everything else is unknown.
57
57
  private static func reference(_ ref: INMediaReference) -> MediaIntentCriteria.Reference {
58
58
  switch ref {
59
- case .currentlyPlaying: return .currentlyPlaying
60
- case .my: return .my
61
- default: return .unknown
59
+ case .currentlyPlaying: .currentlyPlaying
60
+ case .my: .my
61
+ default: .unknown
62
62
  }
63
63
  }
64
64