@wwdrew/expo-apple-music 1.0.0

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 (452) hide show
  1. package/ATTRIBUTION.md +24 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +7 -0
  4. package/README.md +81 -0
  5. package/android/build.gradle +26 -0
  6. package/android/libs/mediaplayback-release-1.1.1.aar +0 -0
  7. package/android/libs/musickitauth-release-1.1.2.aar +0 -0
  8. package/android/src/main/AndroidManifest.xml +16 -0
  9. package/android/src/main/java/expo/modules/applemusic/AndroidCatalogService.kt +86 -0
  10. package/android/src/main/java/expo/modules/applemusic/AndroidDeveloperToken.kt +39 -0
  11. package/android/src/main/java/expo/modules/applemusic/AndroidHistoryService.kt +24 -0
  12. package/android/src/main/java/expo/modules/applemusic/AndroidLibraryMutationsService.kt +30 -0
  13. package/android/src/main/java/expo/modules/applemusic/AndroidLibraryService.kt +61 -0
  14. package/android/src/main/java/expo/modules/applemusic/AndroidPlaybackController.kt +484 -0
  15. package/android/src/main/java/expo/modules/applemusic/AndroidPlaybackObserver.kt +173 -0
  16. package/android/src/main/java/expo/modules/applemusic/AndroidQueueService.kt +78 -0
  17. package/android/src/main/java/expo/modules/applemusic/AndroidRatingsService.kt +27 -0
  18. package/android/src/main/java/expo/modules/applemusic/AndroidRecommendationsService.kt +15 -0
  19. package/android/src/main/java/expo/modules/applemusic/AndroidSubscriptionService.kt +24 -0
  20. package/android/src/main/java/expo/modules/applemusic/AppleMusicErrorCodes.kt +13 -0
  21. package/android/src/main/java/expo/modules/applemusic/AppleMusicErrors.kt +46 -0
  22. package/android/src/main/java/expo/modules/applemusic/AppleMusicHttpMethod.kt +8 -0
  23. package/android/src/main/java/expo/modules/applemusic/AppleMusicJsonMapper.kt +258 -0
  24. package/android/src/main/java/expo/modules/applemusic/AppleMusicNativeLoader.kt +32 -0
  25. package/android/src/main/java/expo/modules/applemusic/AppleMusicRestJson.kt +40 -0
  26. package/android/src/main/java/expo/modules/applemusic/AppleMusicRestQuery.kt +12 -0
  27. package/android/src/main/java/expo/modules/applemusic/AppleMusicRestStack.kt +19 -0
  28. package/android/src/main/java/expo/modules/applemusic/AppleMusicRestTransport.kt +118 -0
  29. package/android/src/main/java/expo/modules/applemusic/AuthenticatedSession.kt +57 -0
  30. package/android/src/main/java/expo/modules/applemusic/BridgeResponses.kt +55 -0
  31. package/android/src/main/java/expo/modules/applemusic/CatalogRestClient.kt +306 -0
  32. package/android/src/main/java/expo/modules/applemusic/ExpoAppleMusicModule.kt +152 -0
  33. package/android/src/main/java/expo/modules/applemusic/HistoryRestClient.kt +60 -0
  34. package/android/src/main/java/expo/modules/applemusic/LibraryIds.kt +6 -0
  35. package/android/src/main/java/expo/modules/applemusic/LibraryMutationsRestClient.kt +95 -0
  36. package/android/src/main/java/expo/modules/applemusic/LibraryRestClient.kt +195 -0
  37. package/android/src/main/java/expo/modules/applemusic/MusicKitAuthContract.kt +78 -0
  38. package/android/src/main/java/expo/modules/applemusic/MusicKitAuthStorage.kt +76 -0
  39. package/android/src/main/java/expo/modules/applemusic/MusicKitTokenProvider.kt +13 -0
  40. package/android/src/main/java/expo/modules/applemusic/PaginationOptions.kt +14 -0
  41. package/android/src/main/java/expo/modules/applemusic/RatingsRestClient.kt +72 -0
  42. package/android/src/main/java/expo/modules/applemusic/RecommendationsRestClient.kt +37 -0
  43. package/android/src/main/java/expo/modules/applemusic/StorefrontRestClient.kt +44 -0
  44. package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeAuth.kt +69 -0
  45. package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeCatalog.kt +76 -0
  46. package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeHistory.kt +35 -0
  47. package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeLibrary.kt +54 -0
  48. package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeLibraryMutations.kt +30 -0
  49. package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgePlayer.kt +89 -0
  50. package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeRatings.kt +29 -0
  51. package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeRecommendations.kt +18 -0
  52. package/app.plugin.js +1 -0
  53. package/build/ExpoAppleMusicModule.web.d.ts +15 -0
  54. package/build/ExpoAppleMusicModule.web.d.ts.map +1 -0
  55. package/build/ExpoAppleMusicModule.web.js +33 -0
  56. package/build/ExpoAppleMusicModule.web.js.map +1 -0
  57. package/build/api/call-native.d.ts +6 -0
  58. package/build/api/call-native.d.ts.map +1 -0
  59. package/build/api/call-native.js +35 -0
  60. package/build/api/call-native.js.map +1 -0
  61. package/build/api/decode-jwt-exp.d.ts +5 -0
  62. package/build/api/decode-jwt-exp.d.ts.map +1 -0
  63. package/build/api/decode-jwt-exp.js +28 -0
  64. package/build/api/decode-jwt-exp.js.map +1 -0
  65. package/build/api/library-ids.d.ts +4 -0
  66. package/build/api/library-ids.d.ts.map +1 -0
  67. package/build/api/library-ids.js +11 -0
  68. package/build/api/library-ids.js.map +1 -0
  69. package/build/api/pagination.d.ts +12 -0
  70. package/build/api/pagination.d.ts.map +1 -0
  71. package/build/api/pagination.js +13 -0
  72. package/build/api/pagination.js.map +1 -0
  73. package/build/api/parse-authorize-result.d.ts +3 -0
  74. package/build/api/parse-authorize-result.d.ts.map +1 -0
  75. package/build/api/parse-authorize-result.js +27 -0
  76. package/build/api/parse-authorize-result.js.map +1 -0
  77. package/build/api/require-music-user-token.d.ts +2 -0
  78. package/build/api/require-music-user-token.d.ts.map +1 -0
  79. package/build/api/require-music-user-token.js +14 -0
  80. package/build/api/require-music-user-token.js.map +1 -0
  81. package/build/api/sync-developer-token.d.ts +4 -0
  82. package/build/api/sync-developer-token.d.ts.map +1 -0
  83. package/build/api/sync-developer-token.js +27 -0
  84. package/build/api/sync-developer-token.js.map +1 -0
  85. package/build/bridge/bridge-methods.d.ts +17 -0
  86. package/build/bridge/bridge-methods.d.ts.map +1 -0
  87. package/build/bridge/bridge-methods.js +71 -0
  88. package/build/bridge/bridge-methods.js.map +1 -0
  89. package/build/bridge/bridge-responses.d.ts +64 -0
  90. package/build/bridge/bridge-responses.d.ts.map +1 -0
  91. package/build/bridge/bridge-responses.js +67 -0
  92. package/build/bridge/bridge-responses.js.map +1 -0
  93. package/build/bridge/handlers/auth-bridge.d.ts +11 -0
  94. package/build/bridge/handlers/auth-bridge.d.ts.map +1 -0
  95. package/build/bridge/handlers/auth-bridge.js +49 -0
  96. package/build/bridge/handlers/auth-bridge.js.map +1 -0
  97. package/build/bridge/handlers/catalog-bridge.d.ts +34 -0
  98. package/build/bridge/handlers/catalog-bridge.d.ts.map +1 -0
  99. package/build/bridge/handlers/catalog-bridge.js +58 -0
  100. package/build/bridge/handlers/catalog-bridge.js.map +1 -0
  101. package/build/bridge/handlers/history-bridge.d.ts +19 -0
  102. package/build/bridge/handlers/history-bridge.d.ts.map +1 -0
  103. package/build/bridge/handlers/history-bridge.js +31 -0
  104. package/build/bridge/handlers/history-bridge.js.map +1 -0
  105. package/build/bridge/handlers/index.d.ts +124 -0
  106. package/build/bridge/handlers/index.d.ts.map +1 -0
  107. package/build/bridge/handlers/index.js +21 -0
  108. package/build/bridge/handlers/index.js.map +1 -0
  109. package/build/bridge/handlers/library-bridge.d.ts +23 -0
  110. package/build/bridge/handlers/library-bridge.d.ts.map +1 -0
  111. package/build/bridge/handlers/library-bridge.js +41 -0
  112. package/build/bridge/handlers/library-bridge.js.map +1 -0
  113. package/build/bridge/handlers/library-mutations-bridge.d.ts +16 -0
  114. package/build/bridge/handlers/library-mutations-bridge.d.ts.map +1 -0
  115. package/build/bridge/handlers/library-mutations-bridge.js +20 -0
  116. package/build/bridge/handlers/library-mutations-bridge.js.map +1 -0
  117. package/build/bridge/handlers/player-bridge.d.ts +18 -0
  118. package/build/bridge/handlers/player-bridge.d.ts.map +1 -0
  119. package/build/bridge/handlers/player-bridge.js +42 -0
  120. package/build/bridge/handlers/player-bridge.js.map +1 -0
  121. package/build/bridge/handlers/ratings-bridge.d.ts +15 -0
  122. package/build/bridge/handlers/ratings-bridge.d.ts.map +1 -0
  123. package/build/bridge/handlers/ratings-bridge.js +16 -0
  124. package/build/bridge/handlers/ratings-bridge.js.map +1 -0
  125. package/build/bridge/handlers/recommendations-bridge.d.ts +10 -0
  126. package/build/bridge/handlers/recommendations-bridge.d.ts.map +1 -0
  127. package/build/bridge/handlers/recommendations-bridge.js +14 -0
  128. package/build/bridge/handlers/recommendations-bridge.js.map +1 -0
  129. package/build/constants/apple-music-error-codes.d.ts +22 -0
  130. package/build/constants/apple-music-error-codes.d.ts.map +1 -0
  131. package/build/constants/apple-music-error-codes.js +21 -0
  132. package/build/constants/apple-music-error-codes.js.map +1 -0
  133. package/build/hooks/use-current-song.d.ts +10 -0
  134. package/build/hooks/use-current-song.d.ts.map +1 -0
  135. package/build/hooks/use-current-song.js +35 -0
  136. package/build/hooks/use-current-song.js.map +1 -0
  137. package/build/hooks/use-is-playing.d.ts +6 -0
  138. package/build/hooks/use-is-playing.d.ts.map +1 -0
  139. package/build/hooks/use-is-playing.js +21 -0
  140. package/build/hooks/use-is-playing.js.map +1 -0
  141. package/build/hooks/use-playback-state.d.ts +12 -0
  142. package/build/hooks/use-playback-state.d.ts.map +1 -0
  143. package/build/hooks/use-playback-state.js +41 -0
  144. package/build/hooks/use-playback-state.js.map +1 -0
  145. package/build/index.d.ts +52 -0
  146. package/build/index.d.ts.map +1 -0
  147. package/build/index.js +44 -0
  148. package/build/index.js.map +1 -0
  149. package/build/mappers/apple-music-json-mapper.d.ts +94 -0
  150. package/build/mappers/apple-music-json-mapper.d.ts.map +1 -0
  151. package/build/mappers/apple-music-json-mapper.js +212 -0
  152. package/build/mappers/apple-music-json-mapper.js.map +1 -0
  153. package/build/modules/auth.d.ts +32 -0
  154. package/build/modules/auth.d.ts.map +1 -0
  155. package/build/modules/auth.js +60 -0
  156. package/build/modules/auth.js.map +1 -0
  157. package/build/modules/catalog.d.ts +46 -0
  158. package/build/modules/catalog.d.ts.map +1 -0
  159. package/build/modules/catalog.js +47 -0
  160. package/build/modules/catalog.js.map +1 -0
  161. package/build/modules/history.d.ts +17 -0
  162. package/build/modules/history.d.ts.map +1 -0
  163. package/build/modules/history.js +28 -0
  164. package/build/modules/history.js.map +1 -0
  165. package/build/modules/library-mutations.d.ts +9 -0
  166. package/build/modules/library-mutations.d.ts.map +1 -0
  167. package/build/modules/library-mutations.js +41 -0
  168. package/build/modules/library-mutations.js.map +1 -0
  169. package/build/modules/library.d.ts +21 -0
  170. package/build/modules/library.d.ts.map +1 -0
  171. package/build/modules/library.js +38 -0
  172. package/build/modules/library.js.map +1 -0
  173. package/build/modules/player.d.ts +53 -0
  174. package/build/modules/player.d.ts.map +1 -0
  175. package/build/modules/player.js +68 -0
  176. package/build/modules/player.js.map +1 -0
  177. package/build/modules/ratings.d.ts +10 -0
  178. package/build/modules/ratings.d.ts.map +1 -0
  179. package/build/modules/ratings.js +37 -0
  180. package/build/modules/ratings.js.map +1 -0
  181. package/build/modules/recommendations.d.ts +7 -0
  182. package/build/modules/recommendations.d.ts.map +1 -0
  183. package/build/modules/recommendations.js +15 -0
  184. package/build/modules/recommendations.js.map +1 -0
  185. package/build/native-module.d.ts +5 -0
  186. package/build/native-module.d.ts.map +1 -0
  187. package/build/native-module.js +5 -0
  188. package/build/native-module.js.map +1 -0
  189. package/build/native-module.web.d.ts +11 -0
  190. package/build/native-module.web.d.ts.map +1 -0
  191. package/build/native-module.web.js +11 -0
  192. package/build/native-module.web.js.map +1 -0
  193. package/build/rest/apple-music-rest-stack.d.ts +21 -0
  194. package/build/rest/apple-music-rest-stack.d.ts.map +1 -0
  195. package/build/rest/apple-music-rest-stack.js +21 -0
  196. package/build/rest/apple-music-rest-stack.js.map +1 -0
  197. package/build/rest/apple-music-rest-transport.d.ts +8 -0
  198. package/build/rest/apple-music-rest-transport.d.ts.map +1 -0
  199. package/build/rest/apple-music-rest-transport.js +2 -0
  200. package/build/rest/apple-music-rest-transport.js.map +1 -0
  201. package/build/rest/catalog-rest-client.d.ts +38 -0
  202. package/build/rest/catalog-rest-client.d.ts.map +1 -0
  203. package/build/rest/catalog-rest-client.js +189 -0
  204. package/build/rest/catalog-rest-client.js.map +1 -0
  205. package/build/rest/history-rest-client.d.ts +37 -0
  206. package/build/rest/history-rest-client.d.ts.map +1 -0
  207. package/build/rest/history-rest-client.js +30 -0
  208. package/build/rest/history-rest-client.js.map +1 -0
  209. package/build/rest/library-ids.d.ts +2 -0
  210. package/build/rest/library-ids.d.ts.map +1 -0
  211. package/build/rest/library-ids.js +2 -0
  212. package/build/rest/library-ids.js.map +1 -0
  213. package/build/rest/library-mutations-rest-client.d.ts +22 -0
  214. package/build/rest/library-mutations-rest-client.d.ts.map +1 -0
  215. package/build/rest/library-mutations-rest-client.js +37 -0
  216. package/build/rest/library-mutations-rest-client.js.map +1 -0
  217. package/build/rest/library-rest-client.d.ts +59 -0
  218. package/build/rest/library-rest-client.d.ts.map +1 -0
  219. package/build/rest/library-rest-client.js +141 -0
  220. package/build/rest/library-rest-client.js.map +1 -0
  221. package/build/rest/ratings-rest-client.d.ts +18 -0
  222. package/build/rest/ratings-rest-client.d.ts.map +1 -0
  223. package/build/rest/ratings-rest-client.js +37 -0
  224. package/build/rest/ratings-rest-client.js.map +1 -0
  225. package/build/rest/recommendations-rest-client.d.ts +32 -0
  226. package/build/rest/recommendations-rest-client.d.ts.map +1 -0
  227. package/build/rest/recommendations-rest-client.js +26 -0
  228. package/build/rest/recommendations-rest-client.js.map +1 -0
  229. package/build/rest/resource-ids-query.d.ts +2 -0
  230. package/build/rest/resource-ids-query.d.ts.map +1 -0
  231. package/build/rest/resource-ids-query.js +10 -0
  232. package/build/rest/resource-ids-query.js.map +1 -0
  233. package/build/rest/rest-json.d.ts +11 -0
  234. package/build/rest/rest-json.d.ts.map +1 -0
  235. package/build/rest/rest-json.js +29 -0
  236. package/build/rest/rest-json.js.map +1 -0
  237. package/build/rest/storefront-rest-client.d.ts +14 -0
  238. package/build/rest/storefront-rest-client.d.ts.map +1 -0
  239. package/build/rest/storefront-rest-client.js +36 -0
  240. package/build/rest/storefront-rest-client.js.map +1 -0
  241. package/build/types/album.d.ts +8 -0
  242. package/build/types/album.d.ts.map +1 -0
  243. package/build/types/album.js +2 -0
  244. package/build/types/album.js.map +1 -0
  245. package/build/types/albums-response.d.ts +5 -0
  246. package/build/types/albums-response.d.ts.map +1 -0
  247. package/build/types/albums-response.js +2 -0
  248. package/build/types/albums-response.js.map +1 -0
  249. package/build/types/android-authorize-options.d.ts +18 -0
  250. package/build/types/android-authorize-options.d.ts.map +1 -0
  251. package/build/types/android-authorize-options.js +2 -0
  252. package/build/types/android-authorize-options.js.map +1 -0
  253. package/build/types/artist.d.ts +9 -0
  254. package/build/types/artist.d.ts.map +1 -0
  255. package/build/types/artist.js +2 -0
  256. package/build/types/artist.js.map +1 -0
  257. package/build/types/auth-status.d.ts +19 -0
  258. package/build/types/auth-status.d.ts.map +1 -0
  259. package/build/types/auth-status.js +18 -0
  260. package/build/types/auth-status.js.map +1 -0
  261. package/build/types/authorize-result.d.ts +8 -0
  262. package/build/types/authorize-result.d.ts.map +1 -0
  263. package/build/types/authorize-result.js +2 -0
  264. package/build/types/authorize-result.js.map +1 -0
  265. package/build/types/catalog-album-tracks.d.ts +5 -0
  266. package/build/types/catalog-album-tracks.d.ts.map +1 -0
  267. package/build/types/catalog-album-tracks.js +2 -0
  268. package/build/types/catalog-album-tracks.js.map +1 -0
  269. package/build/types/catalog-charts.d.ts +25 -0
  270. package/build/types/catalog-charts.d.ts.map +1 -0
  271. package/build/types/catalog-charts.js +7 -0
  272. package/build/types/catalog-charts.js.map +1 -0
  273. package/build/types/catalog-resource-type.d.ts +11 -0
  274. package/build/types/catalog-resource-type.d.ts.map +1 -0
  275. package/build/types/catalog-resource-type.js +10 -0
  276. package/build/types/catalog-resource-type.js.map +1 -0
  277. package/build/types/catalog-search.d.ts +24 -0
  278. package/build/types/catalog-search.d.ts.map +1 -0
  279. package/build/types/catalog-search.js +9 -0
  280. package/build/types/catalog-search.js.map +1 -0
  281. package/build/types/check-subscription.d.ts +31 -0
  282. package/build/types/check-subscription.d.ts.map +1 -0
  283. package/build/types/check-subscription.js +5 -0
  284. package/build/types/check-subscription.js.map +1 -0
  285. package/build/types/library-music-videos.d.ts +5 -0
  286. package/build/types/library-music-videos.d.ts.map +1 -0
  287. package/build/types/library-music-videos.js +2 -0
  288. package/build/types/library-music-videos.js.map +1 -0
  289. package/build/types/library-mutations.d.ts +21 -0
  290. package/build/types/library-mutations.d.ts.map +1 -0
  291. package/build/types/library-mutations.js +8 -0
  292. package/build/types/library-mutations.js.map +1 -0
  293. package/build/types/library-search.d.ts +22 -0
  294. package/build/types/library-search.d.ts.map +1 -0
  295. package/build/types/library-search.js +9 -0
  296. package/build/types/library-search.js.map +1 -0
  297. package/build/types/music-item.d.ts +8 -0
  298. package/build/types/music-item.d.ts.map +1 -0
  299. package/build/types/music-item.js +7 -0
  300. package/build/types/music-item.js.map +1 -0
  301. package/build/types/music-video.d.ts +8 -0
  302. package/build/types/music-video.d.ts.map +1 -0
  303. package/build/types/music-video.js +2 -0
  304. package/build/types/music-video.js.map +1 -0
  305. package/build/types/pagination.d.ts +11 -0
  306. package/build/types/pagination.d.ts.map +1 -0
  307. package/build/types/pagination.js +2 -0
  308. package/build/types/pagination.js.map +1 -0
  309. package/build/types/playback-state.d.ts +9 -0
  310. package/build/types/playback-state.d.ts.map +1 -0
  311. package/build/types/playback-state.js +2 -0
  312. package/build/types/playback-state.js.map +1 -0
  313. package/build/types/playback-status.d.ts +10 -0
  314. package/build/types/playback-status.d.ts.map +1 -0
  315. package/build/types/playback-status.js +9 -0
  316. package/build/types/playback-status.js.map +1 -0
  317. package/build/types/playlist.d.ts +15 -0
  318. package/build/types/playlist.d.ts.map +1 -0
  319. package/build/types/playlist.js +2 -0
  320. package/build/types/playlist.js.map +1 -0
  321. package/build/types/rating.d.ts +34 -0
  322. package/build/types/rating.d.ts.map +1 -0
  323. package/build/types/rating.js +26 -0
  324. package/build/types/rating.js.map +1 -0
  325. package/build/types/recent-resource.d.ts +11 -0
  326. package/build/types/recent-resource.d.ts.map +1 -0
  327. package/build/types/recent-resource.js +2 -0
  328. package/build/types/recent-resource.js.map +1 -0
  329. package/build/types/recommendation.d.ts +37 -0
  330. package/build/types/recommendation.d.ts.map +1 -0
  331. package/build/types/recommendation.js +2 -0
  332. package/build/types/recommendation.js.map +1 -0
  333. package/build/types/song.d.ts +8 -0
  334. package/build/types/song.d.ts.map +1 -0
  335. package/build/types/song.js +2 -0
  336. package/build/types/song.js.map +1 -0
  337. package/build/types/station.d.ts +9 -0
  338. package/build/types/station.d.ts.map +1 -0
  339. package/build/types/station.js +2 -0
  340. package/build/types/station.js.map +1 -0
  341. package/build/types/storefront.d.ts +4 -0
  342. package/build/types/storefront.d.ts.map +1 -0
  343. package/build/types/storefront.js +2 -0
  344. package/build/types/storefront.js.map +1 -0
  345. package/build/types/tracks-from-library.d.ts +11 -0
  346. package/build/types/tracks-from-library.d.ts.map +1 -0
  347. package/build/types/tracks-from-library.js +2 -0
  348. package/build/types/tracks-from-library.js.map +1 -0
  349. package/build/utils/apple-music-error.d.ts +10 -0
  350. package/build/utils/apple-music-error.d.ts.map +1 -0
  351. package/build/utils/apple-music-error.js +13 -0
  352. package/build/utils/apple-music-error.js.map +1 -0
  353. package/build/utils/get-error-message.d.ts +2 -0
  354. package/build/utils/get-error-message.d.ts.map +1 -0
  355. package/build/utils/get-error-message.js +21 -0
  356. package/build/utils/get-error-message.js.map +1 -0
  357. package/build/utils/is-library-item.d.ts +2 -0
  358. package/build/utils/is-library-item.d.ts.map +1 -0
  359. package/build/utils/is-library-item.js +4 -0
  360. package/build/utils/is-library-item.js.map +1 -0
  361. package/build/utils/normalize-resource-ids.d.ts +4 -0
  362. package/build/utils/normalize-resource-ids.d.ts.map +1 -0
  363. package/build/utils/normalize-resource-ids.js +12 -0
  364. package/build/utils/normalize-resource-ids.js.map +1 -0
  365. package/build/web/MusicKitLoader.d.ts +11 -0
  366. package/build/web/MusicKitLoader.d.ts.map +1 -0
  367. package/build/web/MusicKitLoader.js +135 -0
  368. package/build/web/MusicKitLoader.js.map +1 -0
  369. package/build/web/WebAppleMusicApiClient.d.ts +151 -0
  370. package/build/web/WebAppleMusicApiClient.d.ts.map +1 -0
  371. package/build/web/WebAppleMusicApiClient.js +139 -0
  372. package/build/web/WebAppleMusicApiClient.js.map +1 -0
  373. package/build/web/WebAppleMusicRestTransport.d.ts +9 -0
  374. package/build/web/WebAppleMusicRestTransport.d.ts.map +1 -0
  375. package/build/web/WebAppleMusicRestTransport.js +31 -0
  376. package/build/web/WebAppleMusicRestTransport.js.map +1 -0
  377. package/build/web/WebPlaybackController.d.ts +12 -0
  378. package/build/web/WebPlaybackController.d.ts.map +1 -0
  379. package/build/web/WebPlaybackController.js +90 -0
  380. package/build/web/WebPlaybackController.js.map +1 -0
  381. package/build/web/WebPlaybackObserver.d.ts +22 -0
  382. package/build/web/WebPlaybackObserver.d.ts.map +1 -0
  383. package/build/web/WebPlaybackObserver.js +106 -0
  384. package/build/web/WebPlaybackObserver.js.map +1 -0
  385. package/build/web/WebQueueService.d.ts +10 -0
  386. package/build/web/WebQueueService.d.ts.map +1 -0
  387. package/build/web/WebQueueService.js +53 -0
  388. package/build/web/WebQueueService.js.map +1 -0
  389. package/build/web/WebSubscriptionService.d.ts +7 -0
  390. package/build/web/WebSubscriptionService.d.ts.map +1 -0
  391. package/build/web/WebSubscriptionService.js +24 -0
  392. package/build/web/WebSubscriptionService.js.map +1 -0
  393. package/build/web/apple-music-errors.d.ts +11 -0
  394. package/build/web/apple-music-errors.d.ts.map +1 -0
  395. package/build/web/apple-music-errors.js +31 -0
  396. package/build/web/apple-music-errors.js.map +1 -0
  397. package/build/web/extract-music-user-token.d.ts +4 -0
  398. package/build/web/extract-music-user-token.d.ts.map +1 -0
  399. package/build/web/extract-music-user-token.js +15 -0
  400. package/build/web/extract-music-user-token.js.map +1 -0
  401. package/build/web/map-auth-status.d.ts +17 -0
  402. package/build/web/map-auth-status.d.ts.map +1 -0
  403. package/build/web/map-auth-status.js +85 -0
  404. package/build/web/map-auth-status.js.map +1 -0
  405. package/build/web/music-kit-api.d.ts +18 -0
  406. package/build/web/music-kit-api.d.ts.map +1 -0
  407. package/build/web/music-kit-api.js +120 -0
  408. package/build/web/music-kit-api.js.map +1 -0
  409. package/build/web/musickit-types.d.ts +70 -0
  410. package/build/web/musickit-types.d.ts.map +1 -0
  411. package/build/web/musickit-types.js +3 -0
  412. package/build/web/musickit-types.js.map +1 -0
  413. package/build/web/pagination.d.ts +2 -0
  414. package/build/web/pagination.d.ts.map +1 -0
  415. package/build/web/pagination.js +2 -0
  416. package/build/web/pagination.js.map +1 -0
  417. package/expo-module.config.json +19 -0
  418. package/ios/AppleMusicBridgeError.swift +60 -0
  419. package/ios/AppleMusicErrorCodes.swift +11 -0
  420. package/ios/AppleMusicRestClient.swift +213 -0
  421. package/ios/AuthenticatedSession.swift +64 -0
  422. package/ios/BridgePagination.swift +17 -0
  423. package/ios/BridgeResponses.swift +82 -0
  424. package/ios/CatalogSearchStore.swift +13 -0
  425. package/ios/CatalogSearchStoreFactory.swift +31 -0
  426. package/ios/CatalogService.swift +307 -0
  427. package/ios/ExpoAppleMusic.podspec +29 -0
  428. package/ios/ExpoAppleMusicModule.swift +505 -0
  429. package/ios/HistoryService.swift +53 -0
  430. package/ios/LibraryMutationsService.swift +63 -0
  431. package/ios/LibraryService.swift +313 -0
  432. package/ios/MusicItemMapper.swift +171 -0
  433. package/ios/MusicKitAuthStorage.swift +38 -0
  434. package/ios/MusicKitCatalogSearchStore.swift +62 -0
  435. package/ios/PlaybackController.swift +201 -0
  436. package/ios/PlaybackObserver.swift +225 -0
  437. package/ios/QueueService.swift +166 -0
  438. package/ios/RatingsService.swift +66 -0
  439. package/ios/RecommendationsService.swift +34 -0
  440. package/ios/RestCatalogSearchStore.swift +62 -0
  441. package/ios/RestJsonMapper.swift +268 -0
  442. package/ios/StorefrontService.swift +55 -0
  443. package/ios/SubscriptionService.swift +119 -0
  444. package/ios/bridge/ExpoBridgeCatalog.swift +98 -0
  445. package/ios/bridge/ExpoBridgeHistory.swift +71 -0
  446. package/ios/bridge/ExpoBridgeLibrary.swift +93 -0
  447. package/ios/bridge/ExpoBridgeRecommendations.swift +28 -0
  448. package/package.json +89 -0
  449. package/plugin/build/index.d.ts +5 -0
  450. package/plugin/build/index.js +10 -0
  451. package/plugin/build/with-expo-apple-music.d.ts +10 -0
  452. package/plugin/build/with-expo-apple-music.js +50 -0
@@ -0,0 +1,201 @@
1
+ // PlaybackController.swift
2
+ // Encapsulates ApplicationMusicPlayer operations with caching for song info.
3
+
4
+ import AVFoundation
5
+ import Foundation
6
+ import MusicKit
7
+
8
+ @available(iOS 16.0, *)
9
+ final class PlaybackController {
10
+
11
+ // MARK: - Shared Instance
12
+
13
+ static let shared = PlaybackController()
14
+
15
+ // MARK: - Properties
16
+
17
+ private var player: ApplicationMusicPlayer {
18
+ ApplicationMusicPlayer.shared
19
+ }
20
+
21
+ var state: MusicKit.MusicPlayer.State {
22
+ player.state
23
+ }
24
+
25
+ var playbackTime: TimeInterval {
26
+ get { player.playbackTime }
27
+ set { player.playbackTime = newValue }
28
+ }
29
+
30
+ var currentEntry: ApplicationMusicPlayer.Queue.Entry? {
31
+ player.queue.currentEntry
32
+ }
33
+
34
+ // MARK: - Song Info Cache
35
+
36
+ private var cachedSongId: String?
37
+ private var cachedSongInfo: [String: Any]?
38
+
39
+ private lazy var catalogService = CatalogService()
40
+
41
+ // MARK: - Initialization
42
+
43
+ private init() {}
44
+
45
+ /// Clears the song info cache (call when queue changes significantly)
46
+ func clearSongCache() {
47
+ cachedSongId = nil
48
+ cachedSongInfo = nil
49
+ }
50
+
51
+ // MARK: - Audio Session Configuration
52
+
53
+ func configureAudioSession(mixWithOthers: Bool) throws {
54
+ let session = AVAudioSession.sharedInstance()
55
+ if mixWithOthers {
56
+ try session.setCategory(.playback, mode: .default, options: [.mixWithOthers, .duckOthers])
57
+ } else {
58
+ try session.setCategory(.playback, mode: .default)
59
+ }
60
+ try session.setActive(true)
61
+ }
62
+
63
+ // MARK: - Playback Controls
64
+
65
+ func play() async throws {
66
+ try await player.play()
67
+ }
68
+
69
+ func pause() {
70
+ player.pause()
71
+ }
72
+
73
+ func togglePlayback() async throws {
74
+ switch state.playbackStatus {
75
+ case .playing:
76
+ pause()
77
+ case .paused, .stopped, .interrupted:
78
+ try await play()
79
+ default:
80
+ try await play()
81
+ }
82
+ }
83
+
84
+ func skipToNext() async throws {
85
+ try await player.skipToNextEntry()
86
+ }
87
+
88
+ func skipToPrevious() async throws {
89
+ try await player.skipToPreviousEntry()
90
+ }
91
+
92
+ func restartCurrentEntry() {
93
+ player.restartCurrentEntry()
94
+ }
95
+
96
+ func seek(to time: TimeInterval) {
97
+ playbackTime = time
98
+ }
99
+
100
+ // MARK: - Queue Management
101
+
102
+ func setQueue<T: PlayableMusicItem>(_ item: T) async throws {
103
+ player.queue = [item]
104
+ try await player.prepareToPlay()
105
+ }
106
+
107
+ func setQueue<T: PlayableMusicItem>(_ items: [T], startingAt item: T) async throws {
108
+ player.queue = ApplicationMusicPlayer.Queue(for: items, startingAt: item)
109
+ try await player.prepareToPlay()
110
+ }
111
+
112
+ // MARK: - Current Song Info
113
+
114
+ /// Fetches detailed info for the current queue entry from the catalog.
115
+ /// Uses caching to avoid redundant network calls when the song hasn't changed.
116
+ func fetchCurrentSongInfo() async -> [String: Any]? {
117
+ guard let entry = currentEntry else {
118
+ // No current entry - clear cache and return nil
119
+ clearSongCache()
120
+ return nil
121
+ }
122
+
123
+ // Extract the current item's ID
124
+ let currentId: String?
125
+ switch entry.item {
126
+ case .song(let song):
127
+ let idString = String(describing: song.id)
128
+ // Skip if ID is empty (identifiers not yet resolved)
129
+ currentId = idString.isEmpty ? nil : idString
130
+
131
+ case .musicVideo(let musicVideo):
132
+ let idString = String(describing: musicVideo.id)
133
+ currentId = idString.isEmpty ? nil : idString
134
+
135
+ default:
136
+ currentId = nil
137
+ }
138
+
139
+ // If no valid ID, return cached info (if any) or nil
140
+ guard let currentId = currentId else {
141
+ return cachedSongInfo
142
+ }
143
+
144
+ // Return cached info if same song
145
+ if currentId == cachedSongId, let cached = cachedSongInfo {
146
+ return cached
147
+ }
148
+
149
+ // Prefer metadata from the queue entry (avoids catalog re-fetch by cloud playback ids).
150
+ let songInfo: [String: Any]?
151
+ switch entry.item {
152
+ case .song(let song):
153
+ songInfo = await songInfoForQueueEntry(song)
154
+
155
+ case .musicVideo(let musicVideo):
156
+ songInfo = await musicVideoInfoForQueueEntry(musicVideo)
157
+
158
+ default:
159
+ songInfo = nil
160
+ }
161
+
162
+ // Update cache only if we got valid info
163
+ if let songInfo = songInfo {
164
+ cachedSongId = currentId
165
+ cachedSongInfo = songInfo
166
+ }
167
+
168
+ return songInfo ?? cachedSongInfo
169
+ }
170
+
171
+ private func songInfoForQueueEntry(_ song: Song) async -> [String: Any] {
172
+ let mapped = MusicItemMapper.map(song)
173
+ if hasDisplayMetadata(mapped) {
174
+ return mapped
175
+ }
176
+ return await fetchSongDetailsFallback(song.id) ?? mapped
177
+ }
178
+
179
+ private func musicVideoInfoForQueueEntry(_ musicVideo: MusicVideo) async -> [String: Any] {
180
+ let mapped = MusicItemMapper.map(musicVideo)
181
+ if hasDisplayMetadata(mapped) {
182
+ return mapped
183
+ }
184
+ return await fetchMusicVideoDetailsFallback(musicVideo.id) ?? mapped
185
+ }
186
+
187
+ private func hasDisplayMetadata(_ mapped: [String: Any]) -> Bool {
188
+ let title = mapped["title"] as? String ?? ""
189
+ return !title.isEmpty
190
+ }
191
+
192
+ private func fetchSongDetailsFallback(_ id: MusicItemID) async -> [String: Any]? {
193
+ guard let song = try? await catalogService.fetchSong(id: id) else { return nil }
194
+ return MusicItemMapper.map(song)
195
+ }
196
+
197
+ private func fetchMusicVideoDetailsFallback(_ id: MusicItemID) async -> [String: Any]? {
198
+ guard let video = try? await catalogService.fetchMusicVideo(id: id) else { return nil }
199
+ return MusicItemMapper.map(video)
200
+ }
201
+ }
@@ -0,0 +1,225 @@
1
+ // PlaybackObserver.swift
2
+ // Modern Swift Concurrency-based observation for playback state and time updates.
3
+ // Performance-optimized: network calls run off main thread, only UI updates on MainActor.
4
+
5
+ import Combine
6
+ import Foundation
7
+ import MusicKit
8
+
9
+ /// Protocol for receiving playback observation events.
10
+ @available(iOS 16.0, *)
11
+ protocol PlaybackObserverDelegate: AnyObject {
12
+ @MainActor func playbackStateDidChange(_ state: PlaybackObserver.PlaybackInfo)
13
+ @MainActor func currentSongDidChange(_ songInfo: [String: Any]?)
14
+ @MainActor func playbackTimeDidUpdate(_ time: TimeInterval)
15
+ }
16
+
17
+ @available(iOS 16.0, *)
18
+ final class PlaybackObserver {
19
+
20
+ // MARK: - Types
21
+
22
+ struct PlaybackInfo {
23
+ let playbackStatus: String
24
+ let playbackRate: Float
25
+ let playbackTime: TimeInterval
26
+ let currentSong: [String: Any]?
27
+ }
28
+
29
+ // MARK: - Properties
30
+
31
+ weak var delegate: PlaybackObserverDelegate?
32
+
33
+ private let playbackController: PlaybackController
34
+ private var stateObservationTask: Task<Void, Never>?
35
+ private var queueObservationTask: Task<Void, Never>?
36
+ private var timeUpdateTask: Task<Void, Never>?
37
+
38
+ /// Thread-safe access to last reported status using actor isolation
39
+ private let statusTracker = StatusTracker()
40
+
41
+ /// Time update interval - 1 second provides good UX with less main thread pressure
42
+ private let timeUpdateInterval: TimeInterval = 1.0
43
+
44
+ // MARK: - Initialization
45
+
46
+ init(playbackController: PlaybackController = .shared) {
47
+ self.playbackController = playbackController
48
+ }
49
+
50
+ deinit {
51
+ stopObserving()
52
+ }
53
+
54
+ // MARK: - Observation Lifecycle
55
+
56
+ func startObserving() {
57
+ startStateObservation()
58
+ startQueueObservation()
59
+ // Start time updates if already playing
60
+ if playbackController.state.playbackStatus == .playing {
61
+ startTimeUpdates()
62
+ }
63
+ }
64
+
65
+ func stopObserving() {
66
+ stateObservationTask?.cancel()
67
+ queueObservationTask?.cancel()
68
+ timeUpdateTask?.cancel()
69
+ stateObservationTask = nil
70
+ queueObservationTask = nil
71
+ timeUpdateTask = nil
72
+ // Note: statusTracker doesn't need explicit reset - it will be deallocated with self
73
+ }
74
+
75
+ // MARK: - State Observation (Swift Concurrency)
76
+
77
+ private func startStateObservation() {
78
+ stateObservationTask?.cancel()
79
+
80
+ // Capture dependencies outside the task to avoid capturing self
81
+ let playbackController = self.playbackController
82
+ let statusTracker = self.statusTracker
83
+ weak var weakDelegate = self.delegate
84
+ weak var weakSelf = self
85
+
86
+ stateObservationTask = Task.detached {
87
+ // Use AsyncStream to bridge objectWillChange
88
+ let stateStream = AsyncStream<Void> { continuation in
89
+ let cancellable = ApplicationMusicPlayer.shared.state.objectWillChange.sink { _ in
90
+ continuation.yield()
91
+ }
92
+ continuation.onTermination = { _ in
93
+ cancellable.cancel()
94
+ }
95
+ }
96
+
97
+ for await _ in stateStream {
98
+ guard !Task.isCancelled else { break }
99
+
100
+ let state = playbackController.state
101
+ let currentStatus = state.playbackStatus
102
+
103
+ // Check if status actually changed (thread-safe via actor)
104
+ let shouldEmit = await statusTracker.updateIfChanged(currentStatus)
105
+ guard shouldEmit, !Task.isCancelled else { continue }
106
+
107
+ // Fetch song info on background thread
108
+ let songInfo = await playbackController.fetchCurrentSongInfo()
109
+ guard !Task.isCancelled else { break }
110
+
111
+ let info = PlaybackInfo(
112
+ playbackStatus: MusicItemMapper.describePlaybackStatus(currentStatus),
113
+ playbackRate: state.playbackRate,
114
+ playbackTime: playbackController.playbackTime,
115
+ currentSong: songInfo
116
+ )
117
+
118
+ // Only hop to main thread for the UI callback
119
+ let delegate = weakDelegate
120
+ let observer = weakSelf
121
+ await MainActor.run {
122
+ delegate?.playbackStateDidChange(info)
123
+
124
+ // Manage time updates based on playback state
125
+ if currentStatus == .playing {
126
+ observer?.startTimeUpdates()
127
+ } else {
128
+ observer?.stopTimeUpdates()
129
+ let time = playbackController.playbackTime
130
+ delegate?.playbackTimeDidUpdate(time.isNaN ? 0 : time)
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ private func startQueueObservation() {
138
+ queueObservationTask?.cancel()
139
+
140
+ // Capture dependencies outside the task to avoid capturing self
141
+ let playbackController = self.playbackController
142
+ weak var weakDelegate = self.delegate
143
+
144
+ queueObservationTask = Task.detached {
145
+ let queueStream = AsyncStream<Void> { continuation in
146
+ let cancellable = ApplicationMusicPlayer.shared.queue.objectWillChange.sink { _ in
147
+ continuation.yield()
148
+ }
149
+ continuation.onTermination = { _ in
150
+ cancellable.cancel()
151
+ }
152
+ }
153
+
154
+ for await _ in queueStream {
155
+ guard !Task.isCancelled else { break }
156
+
157
+ // Small delay to let queue settle (on background thread)
158
+ try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
159
+ guard !Task.isCancelled else { break }
160
+
161
+ // Fetch song info on background thread
162
+ let songInfo = await playbackController.fetchCurrentSongInfo()
163
+ guard !Task.isCancelled else { break }
164
+
165
+ // Only hop to main thread for the UI callback
166
+ let delegate = weakDelegate
167
+ await MainActor.run {
168
+ delegate?.currentSongDidChange(songInfo)
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ // MARK: - Time Updates (Detached Task-based Timer)
175
+
176
+ private func startTimeUpdates() {
177
+ guard timeUpdateTask == nil else { return }
178
+
179
+ // Capture dependencies outside the task to avoid capturing self
180
+ let playbackController = self.playbackController
181
+ let timeUpdateInterval = self.timeUpdateInterval
182
+ weak var weakDelegate = self.delegate
183
+
184
+ timeUpdateTask = Task.detached {
185
+ while !Task.isCancelled {
186
+ let time = playbackController.playbackTime
187
+ let safeTime = time.isNaN ? 0 : time
188
+
189
+ // Minimal main thread work - just the delegate call
190
+ let delegate = weakDelegate
191
+ await MainActor.run {
192
+ delegate?.playbackTimeDidUpdate(safeTime)
193
+ }
194
+
195
+ try? await Task.sleep(nanoseconds: UInt64(timeUpdateInterval * 1_000_000_000))
196
+ }
197
+ }
198
+ }
199
+
200
+ private func stopTimeUpdates() {
201
+ timeUpdateTask?.cancel()
202
+ timeUpdateTask = nil
203
+ }
204
+ }
205
+
206
+ // MARK: - Status Tracker Actor
207
+
208
+ /// Thread-safe tracker for last reported playback status
209
+ @available(iOS 16.0, *)
210
+ private actor StatusTracker {
211
+ private var lastReportedStatus: MusicPlayer.PlaybackStatus?
212
+
213
+ /// Returns true if the status changed and was updated
214
+ func updateIfChanged(_ newStatus: MusicPlayer.PlaybackStatus) -> Bool {
215
+ if lastReportedStatus != newStatus {
216
+ lastReportedStatus = newStatus
217
+ return true
218
+ }
219
+ return false
220
+ }
221
+
222
+ func reset() {
223
+ lastReportedStatus = nil
224
+ }
225
+ }
@@ -0,0 +1,166 @@
1
+ // QueueService.swift
2
+ // Management for both catalog and library items.
3
+
4
+ import Foundation
5
+ import MusicKit
6
+
7
+ @available(iOS 16.0, *)
8
+ final class QueueService {
9
+
10
+ // MARK: - Dependencies
11
+
12
+ private let playbackController: PlaybackController
13
+ private let catalogService: CatalogService
14
+
15
+ private func makeLibraryService() -> LibraryService {
16
+ LibraryService()
17
+ }
18
+
19
+ // MARK: - Initialization
20
+
21
+ init(
22
+ playbackController: PlaybackController = .shared,
23
+ catalogService: CatalogService = CatalogService()
24
+ ) {
25
+ self.playbackController = playbackController
26
+ self.catalogService = catalogService
27
+ }
28
+
29
+ // MARK: - Queue Setup
30
+
31
+ enum MediaType: String {
32
+ case song
33
+ case album
34
+ case playlist
35
+ case station
36
+ }
37
+
38
+ func setQueue(itemId: String, type: String) async throws {
39
+ let musicItemId = MusicItemID(itemId)
40
+ let isLibrary = Self.isLibraryId(itemId)
41
+
42
+ guard let mediaType = MediaType(rawValue: type) else {
43
+ throw QueueServiceError.unknownMediaType(type)
44
+ }
45
+
46
+ if isLibrary {
47
+ try await setLibraryQueue(id: musicItemId, type: mediaType)
48
+ } else {
49
+ try await setCatalogQueue(id: musicItemId, type: mediaType)
50
+ }
51
+ }
52
+
53
+ // MARK: - Catalog Queue
54
+
55
+ private func setCatalogQueue(id: MusicItemID, type: MediaType) async throws {
56
+ switch type {
57
+ case .song:
58
+ guard let song = try await catalogService.fetchSong(id: id) else {
59
+ throw QueueServiceError.itemNotFound("Song", inLibrary: false)
60
+ }
61
+ try await playbackController.setQueue(song)
62
+
63
+ case .album:
64
+ guard let album = try await catalogService.fetchAlbum(id: id) else {
65
+ throw QueueServiceError.itemNotFound("Album", inLibrary: false)
66
+ }
67
+ try await playbackController.setQueue(album)
68
+
69
+ case .playlist:
70
+ guard let playlist = try await catalogService.fetchPlaylist(id: id) else {
71
+ throw QueueServiceError.itemNotFound("Playlist", inLibrary: false)
72
+ }
73
+ try await playbackController.setQueue(playlist)
74
+
75
+ case .station:
76
+ guard let station = try await catalogService.fetchStation(id: id) else {
77
+ throw QueueServiceError.itemNotFound("Station", inLibrary: false)
78
+ }
79
+ try await playbackController.setQueue(station)
80
+ }
81
+ }
82
+
83
+ // MARK: - Library Queue
84
+
85
+ private func setLibraryQueue(id: MusicItemID, type: MediaType) async throws {
86
+ let service = makeLibraryService()
87
+
88
+ switch type {
89
+ case .song:
90
+ guard let song = try await service.fetchSong(id: id) else {
91
+ throw QueueServiceError.itemNotFound("Song", inLibrary: true)
92
+ }
93
+ try await playbackController.setQueue(song)
94
+
95
+ case .album:
96
+ guard let album = try await service.fetchAlbum(id: id) else {
97
+ throw QueueServiceError.itemNotFound("Album", inLibrary: true)
98
+ }
99
+ try await playbackController.setQueue(album)
100
+
101
+ case .playlist:
102
+ guard let playlist = try await service.fetchPlaylist(id: id) else {
103
+ throw QueueServiceError.itemNotFound("Playlist", inLibrary: true)
104
+ }
105
+ try await playbackController.setQueue(playlist)
106
+
107
+ case .station:
108
+ // Stations are typically not in the library
109
+ throw QueueServiceError.unsupportedLibraryType("station")
110
+ }
111
+ }
112
+
113
+ // MARK: - Library Playback with Starting Position
114
+
115
+ func playLibrarySong(musicUserToken: String, songId: String) async throws {
116
+ let service = makeLibraryService()
117
+ let id = MusicItemID(songId)
118
+ guard let song = try await service.fetchSong(id: id) else {
119
+ throw QueueServiceError.itemNotFound("Song", inLibrary: true)
120
+ }
121
+ try await playbackController.setQueue(song)
122
+ }
123
+
124
+ func playLibraryPlaylist(musicUserToken: String, playlistId: String, startingAt index: Int) async throws {
125
+ let service = makeLibraryService()
126
+ let id = MusicItemID(playlistId)
127
+ guard let playlist = try await service.fetchPlaylist(id: id) else {
128
+ throw QueueServiceError.itemNotFound("Playlist", inLibrary: true)
129
+ }
130
+
131
+ let songs = try await service.extractSongs(from: playlist)
132
+ guard !songs.isEmpty else {
133
+ throw LibraryServiceError.noSongsInPlaylist
134
+ }
135
+
136
+ let startIndex = (index >= 0 && index < songs.count) ? index : 0
137
+ try await playbackController.setQueue(songs, startingAt: songs[startIndex])
138
+ }
139
+
140
+ // MARK: - Helpers
141
+
142
+ /// Checks if an ID is a library ID (starts with "l.", "i.", or "p.")
143
+ static func isLibraryId(_ itemId: String) -> Bool {
144
+ itemId.hasPrefix("l.") || itemId.hasPrefix("i.") || itemId.hasPrefix("p.")
145
+ }
146
+ }
147
+
148
+ // MARK: - Errors
149
+
150
+ enum QueueServiceError: LocalizedError {
151
+ case unknownMediaType(String)
152
+ case itemNotFound(String, inLibrary: Bool)
153
+ case unsupportedLibraryType(String)
154
+
155
+ var errorDescription: String? {
156
+ switch self {
157
+ case .unknownMediaType(let type):
158
+ return "Unknown media type: \(type)"
159
+ case .itemNotFound(let item, let inLibrary):
160
+ let source = inLibrary ? "library" : "catalog"
161
+ return "\(item) not found in \(source)"
162
+ case .unsupportedLibraryType(let type):
163
+ return "Unsupported library media type: \(type)"
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,66 @@
1
+ // RatingsService.swift
2
+ // Personal ratings and favorites via Apple Music REST API.
3
+
4
+ import Foundation
5
+
6
+ @available(iOS 16.0, *)
7
+ final class RatingsService {
8
+
9
+ func getRating(musicUserToken: String, resourceType: String, id: String) async throws -> [String: Any]? {
10
+ do {
11
+ let json = try await AppleMusicRestClient.get(
12
+ path: "/v1/me/ratings/\(resourceType)/\(id)",
13
+ musicUserToken: musicUserToken
14
+ )
15
+ return RestJsonMapper.mapRating(json)
16
+ } catch let error as AppleMusicRestClient.RestError {
17
+ if case .apiError(let message) = error, message.contains("(404)") {
18
+ return nil
19
+ }
20
+ throw error
21
+ }
22
+ }
23
+
24
+ func setRating(musicUserToken: String, resourceType: String, id: String, value: Int) async throws -> [String: Any] {
25
+ let body: [String: Any] = [
26
+ "type": "rating",
27
+ "attributes": ["value": value],
28
+ ]
29
+ let json = try await AppleMusicRestClient.request(
30
+ method: .put,
31
+ path: "/v1/me/ratings/\(resourceType)/\(id)",
32
+ musicUserToken: musicUserToken,
33
+ body: body
34
+ )
35
+ guard let rating = RestJsonMapper.mapRating(json) else {
36
+ throw AppleMusicRestClient.RestError.invalidResponse
37
+ }
38
+ return rating
39
+ }
40
+
41
+ func clearRating(musicUserToken: String, resourceType: String, id: String) async throws {
42
+ _ = try await AppleMusicRestClient.request(
43
+ method: .delete,
44
+ path: "/v1/me/ratings/\(resourceType)/\(id)",
45
+ musicUserToken: musicUserToken
46
+ )
47
+ }
48
+
49
+ func addToFavorites(musicUserToken: String, resourceIds: [String: [String]]) async throws {
50
+ _ = try await AppleMusicRestClient.request(
51
+ method: .post,
52
+ path: "/v1/me/favorites",
53
+ musicUserToken: musicUserToken,
54
+ query: RestJsonMapper.buildIdsQuery(resourceIds)
55
+ )
56
+ }
57
+
58
+ func removeFromFavorites(musicUserToken: String, resourceIds: [String: [String]]) async throws {
59
+ _ = try await AppleMusicRestClient.request(
60
+ method: .delete,
61
+ path: "/v1/me/favorites",
62
+ musicUserToken: musicUserToken,
63
+ query: RestJsonMapper.buildIdsQuery(resourceIds)
64
+ )
65
+ }
66
+ }
@@ -0,0 +1,34 @@
1
+ // RecommendationsService.swift
2
+ // Personal recommendations and Replay via Apple Music REST.
3
+
4
+ import Foundation
5
+
6
+ @available(iOS 16.0, *)
7
+ final class RecommendationsService {
8
+
9
+ func getRecommendations(musicUserToken: String, ids: [String]?) async throws -> [[String: Any]] {
10
+ var query: [String: String] = [:]
11
+ if let ids, !ids.isEmpty {
12
+ query["ids"] = ids.joined(separator: ",")
13
+ }
14
+ let data = try await AppleMusicRestClient.getDataArray(
15
+ path: "/v1/me/recommendations",
16
+ musicUserToken: musicUserToken,
17
+ query: query
18
+ )
19
+ return data.map(RestJsonMapper.mapRecommendation)
20
+ }
21
+
22
+ func getReplay(musicUserToken: String, year: Int?) async throws -> [[String: Any]] {
23
+ var query: [String: String] = [:]
24
+ if let year {
25
+ query["filter[year]"] = "\(year)"
26
+ }
27
+ let data = try await AppleMusicRestClient.getDataArray(
28
+ path: "/v1/me/music-summaries",
29
+ musicUserToken: musicUserToken,
30
+ query: query
31
+ )
32
+ return data.map(RestJsonMapper.mapReplaySummary)
33
+ }
34
+ }