@wwdrew/expo-apple-music 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -4,13 +4,32 @@ Cross-platform Apple Music API for **Expo** (SDK 56 · iOS 16.4+ · Android · W
4
4
 
5
5
  Inspired by [`@lomray/react-native-apple-music`](https://github.com/Lomray-Software/react-native-apple-music) — not a drop-in replacement. See [ATTRIBUTION.md](./ATTRIBUTION.md).
6
6
 
7
+ ## Android: Apple's SDK is your responsibility
8
+
9
+ **If you target Android, you must obtain Apple's MusicKit SDK binaries yourself. This package does not include them, cannot ship them on npm, and there is no way to skip this step.**
10
+
11
+ Apple's MusicKit for Android is distributed as two proprietary `.aar` files (authentication + playback). They are gated behind an [Apple Developer](https://developer.apple.com/account/) login and **cannot be redistributed** by third-party libraries. `npx expo install @wwdrew/expo-apple-music` alone is **not** enough for Android — prebuild will fail until you:
12
+
13
+ 1. **Download** the [MusicKit SDK for Android](https://developer.apple.com/musickit/) from Apple (Developer account required).
14
+ 2. **Store** both `.aar` files in a directory inside **your app repo** (for example `./vendor/apple-musickit-android/` — typically gitignored).
15
+ 3. **Configure** the config plugin with `androidMusicKitAarDir` pointing at that directory.
16
+
17
+ At `npx expo prebuild` for Android, the plugin copies those files into the native build. If the directory is missing or either file is absent, **the build stops with an error**. There is no stub mode, no automatic download, and no fallback.
18
+
19
+ | File | Purpose |
20
+ | --- | --- |
21
+ | `musickitauth-release-1.1.2.aar` | Apple Music sign-in |
22
+ | `mediaplayback-release-1.1.1.aar` | Playback |
23
+
24
+ **iOS and web are unaffected** — this requirement applies only to Android native builds. Full setup: **[docs/GETTING_STARTED.md](./docs/GETTING_STARTED.md)**.
25
+
7
26
  ## Install
8
27
 
9
28
  ```sh
10
29
  npx expo install @wwdrew/expo-apple-music
11
30
  ```
12
31
 
13
- Add the config plugin and enable **MusicKit** on your App ID in the Apple Developer portal. Full steps: **[docs/GETTING_STARTED.md](./docs/GETTING_STARTED.md)**.
32
+ Add the config plugin and enable **MusicKit** on your App ID in the Apple Developer portal. See **[docs/GETTING_STARTED.md](./docs/GETTING_STARTED.md)** for the full checklist (JWT, iOS entitlements, web origin, etc.).
14
33
 
15
34
  ```ts
16
35
  // app.config.ts
@@ -18,7 +37,11 @@ import expoAppleMusic from '@wwdrew/expo-apple-music/plugin';
18
37
 
19
38
  export default {
20
39
  plugins: [
21
- expoAppleMusic({ musicUsageDescription: 'We use Apple Music in this app.' }),
40
+ expoAppleMusic({
41
+ musicUsageDescription: 'We use Apple Music in this app.',
42
+ // Required for Android prebuild — see "Android: Apple's SDK is your responsibility" above
43
+ androidMusicKitAarDir: './vendor/apple-musickit-android',
44
+ }),
22
45
  ],
23
46
  };
24
47
  ```
@@ -37,7 +60,7 @@ if (status === AuthStatus.AUTHORIZED && musicUserToken) {
37
60
  }
38
61
  ```
39
62
 
40
- **Developer JWT:** your app signs and serves it — not included in this package. Local dev: clone the repo and use `npm run dev-token` ([docs/CLI.md](./docs/CLI.md)).
63
+ **Developer JWT:** your app signs and serves it — not included in this package. Local dev: clone the repo and use `yarn dev-token` ([docs/CLI.md](./docs/CLI.md)).
41
64
 
42
65
  ## Documentation
43
66
 
@@ -46,7 +69,7 @@ All guides live in **[docs/](./docs/)** (browse on GitHub):
46
69
  | | |
47
70
  | --- | --- |
48
71
  | **[Getting started](./docs/GETTING_STARTED.md)** | Install → authorize → search → play |
49
- | **[Building locally](./docs/BUILDING_LOCALLY.md)** | Clone, Android `.aar` libs, example app |
72
+ | **[Building locally](./docs/BUILDING_LOCALLY.md)** | Clone, example app, Android `.aar` setup |
50
73
  | **[Auth](./docs/AUTH.md)** | JWT, `AuthStatus`, platform requirements |
51
74
  | **[iOS setup](./docs/IOS_SETUP.md)** | Portal, signing, entitlements |
52
75
  | **[API coverage](./docs/APPLE_MUSIC_API.md)** | Per-method iOS / Android / web matrix |
@@ -68,11 +91,10 @@ Details: [docs/APPLE_MUSIC_API.md](./docs/APPLE_MUSIC_API.md).
68
91
 
69
92
  ## Building locally (repo clone)
70
93
 
71
- The **example app** and Android native builds need Apples MusicKit Android `.aar` libraries in `android/libs/`. Those binaries are **gitignored** and are **not** in the repository — download them from Apple. Full steps: **[docs/BUILDING_LOCALLY.md](./docs/BUILDING_LOCALLY.md)**.
94
+ Clone this repo to run the example app or contribute. The same Android SDK requirement applies: place Apple's `.aar` files in `example/vendor/apple-musickit-android/` before Android prebuild. See **[docs/BUILDING_LOCALLY.md](./docs/BUILDING_LOCALLY.md)**.
72
95
 
73
96
  ```sh
74
- # After placing mediaplayback-release-1.1.1.aar and musickitauth-release-1.1.2.aar in android/libs/
75
- npm run dev-token -- --write-env example/.env.local
97
+ yarn dev-token -- --write-env example/.env.local
76
98
  cd example && npx expo start
77
99
  ```
78
100
 
@@ -22,20 +22,24 @@ class ExpoAppleMusicModule : Module() {
22
22
  private lateinit var authLauncher: AppContextActivityResultLauncher<MusicKitAuthInput, MusicKitAuthOutput>
23
23
  private var playbackObserver: AndroidPlaybackObserver? = null
24
24
 
25
+ @Volatile
26
+ private var playbackErrorHandlerWired = false
27
+
25
28
  private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
26
29
 
27
30
  private val reactContext
28
31
  get() = requireNotNull(appContext.reactContext) { "React Application Context is null" }
29
32
 
30
33
  private val playbackController: AndroidPlaybackController
31
- get() =
32
- AndroidPlaybackController.getInstance(reactContext).also { controller ->
33
- if (controller.playbackErrorHandler == null) {
34
- controller.playbackErrorHandler = { error, operation ->
35
- emitPlaybackError(error, operation)
36
- }
37
- }
38
- }
34
+ get() = AndroidPlaybackController.getInstance(reactContext)
35
+
36
+ private fun wirePlaybackErrorHandlerOnce() {
37
+ if (playbackErrorHandlerWired) return
38
+ AndroidPlaybackController.getInstance(reactContext).playbackErrorHandler = { error, operation ->
39
+ emitPlaybackError(error, operation)
40
+ }
41
+ playbackErrorHandlerWired = true
42
+ }
39
43
 
40
44
  private val catalogService: AndroidCatalogService
41
45
  get() = AndroidCatalogService(reactContext)
@@ -72,6 +76,7 @@ class ExpoAppleMusicModule : Module() {
72
76
  )
73
77
 
74
78
  OnStartObserving {
79
+ wirePlaybackErrorHandlerOnce()
75
80
  val observer = AndroidPlaybackObserver(reactContext)
76
81
  observer.delegate =
77
82
  object : AndroidPlaybackObserverDelegate {
@@ -107,6 +112,7 @@ class ExpoAppleMusicModule : Module() {
107
112
  appContext.throwingActivity
108
113
  },
109
114
  )
115
+ wirePlaybackErrorHandlerOnce()
110
116
  }
111
117
 
112
118
  registerAuthBridge(
@@ -2,468 +2,22 @@ import ExpoModulesCore
2
2
 
3
3
  @available(iOS 16.0, *)
4
4
  public class ExpoAppleMusicModule: Module {
5
- private let playbackController = PlaybackController.shared
6
- private let subscriptionService = SubscriptionService()
7
- private let catalogService = CatalogService()
8
- private lazy var queueService = QueueService(
5
+ let playbackController = PlaybackController.shared
6
+ let subscriptionService = SubscriptionService()
7
+ let catalogService = CatalogService()
8
+ lazy var queueService = QueueService(
9
9
  playbackController: playbackController,
10
10
  catalogService: catalogService
11
11
  )
12
- private lazy var libraryService = LibraryService()
13
- private lazy var historyService = HistoryService()
14
- private lazy var ratingsService = RatingsService()
15
- private lazy var libraryMutationsService = LibraryMutationsService()
16
- private lazy var recommendationsService = RecommendationsService()
12
+ lazy var libraryService = LibraryService()
13
+ lazy var historyService = HistoryService()
14
+ lazy var ratingsService = RatingsService()
15
+ lazy var libraryMutationsService = LibraryMutationsService()
16
+ lazy var recommendationsService = RecommendationsService()
17
17
 
18
- private var playbackObserver: PlaybackObserver?
18
+ var playbackObserver: PlaybackObserver?
19
19
 
20
- public func definition() -> ModuleDefinition {
21
- Name("ExpoAppleMusic")
22
-
23
- Events(
24
- "onPlaybackStateChange",
25
- "onCurrentSongChange",
26
- "onPlaybackTimeUpdate",
27
- "onPlaybackError"
28
- )
29
-
30
- OnStartObserving {
31
- let observer = PlaybackObserver(playbackController: self.playbackController)
32
- observer.delegate = self
33
- observer.startObserving()
34
- self.playbackObserver = observer
35
- }
36
-
37
- OnStopObserving {
38
- self.playbackObserver?.stopObserving()
39
- self.playbackObserver = nil
40
- }
41
-
42
- // MARK: - Auth
43
-
44
- AsyncFunction("setDeveloperToken") { (token: String) in
45
- MusicKitAuthStorage.saveDeveloperToken(token)
46
- }
47
-
48
- AsyncFunction("authorization") { (developerToken: String?, _ startScreenMessage: String?, _ hideStartScreen: Bool?) -> [String: Any?] in
49
- if let token = developerToken, !token.isEmpty {
50
- MusicKitAuthStorage.saveDeveloperToken(token)
51
- }
52
- let status = await self.subscriptionService.requestAuthorization()
53
- var musicUserToken: String? = nil
54
- if status == .authorized, let token = developerToken, !token.isEmpty {
55
- musicUserToken = await self.subscriptionService.fetchMusicUserToken(developerToken: token)
56
- }
57
- return ["status": status.rawValue, "musicUserToken": musicUserToken]
58
- }
59
-
60
- AsyncFunction("checkSubscription") { (musicUserToken: String) -> [String: Any] in
61
- do {
62
- let details = try await self.subscriptionService.checkSubscription()
63
- return details.toDictionary()
64
- } catch {
65
- if let subError = SubscriptionService.wrapSubscriptionError(error) {
66
- throw Exception(
67
- name: subError.code,
68
- description: subError.message,
69
- code: subError.code
70
- )
71
- }
72
- throw AppleMusicBridgeError.exception(from: error)
73
- }
74
- }
75
-
76
- AsyncFunction("getStorefront") { (musicUserToken: String) -> [String: Any] in
77
- try await AppleMusicBridgeError.rethrow {
78
- let id = try await StorefrontService.getStorefrontId(musicUserToken: musicUserToken)
79
- return BridgeResponses.storefront(id: id)
80
- }
81
- }
82
-
83
- // MARK: - Catalog
84
-
85
- AsyncFunction("catalogSearch") { (term: String, types: [String], options: [String: Any]) -> [String: Any] in
86
- try await ExpoBridgeCatalog.catalogSearch(
87
- service: self.catalogService,
88
- term: term,
89
- types: types,
90
- options: options as NSDictionary
91
- )
92
- }
93
-
94
- AsyncFunction("getCatalogSong") { (id: String) -> [String: Any] in
95
- try await AppleMusicBridgeError.rethrow {
96
- try await self.catalogService.getSong(id: id)
97
- }
98
- }
99
-
100
- AsyncFunction("getCatalogAlbum") { (id: String) -> [String: Any] in
101
- try await AppleMusicBridgeError.rethrow {
102
- try await self.catalogService.getAlbum(id: id)
103
- }
104
- }
105
-
106
- AsyncFunction("getCatalogArtist") { (id: String) -> [String: Any] in
107
- try await AppleMusicBridgeError.rethrow {
108
- try await self.catalogService.getArtist(id: id)
109
- }
110
- }
111
-
112
- AsyncFunction("getCatalogPlaylist") { (id: String) -> [String: Any] in
113
- try await AppleMusicBridgeError.rethrow {
114
- try await self.catalogService.getPlaylist(id: id)
115
- }
116
- }
117
-
118
- AsyncFunction("getCatalogStation") { (id: String) -> [String: Any] in
119
- try await AppleMusicBridgeError.rethrow {
120
- try await self.catalogService.getStation(id: id)
121
- }
122
- }
123
-
124
- AsyncFunction("getCatalogMusicVideo") { (id: String) -> [String: Any] in
125
- try await AppleMusicBridgeError.rethrow {
126
- try await self.catalogService.getMusicVideo(id: id)
127
- }
128
- }
129
-
130
- AsyncFunction("getCatalogAlbumTracks") { (albumId: String, options: [String: Any]) -> [String: Any] in
131
- try await ExpoBridgeCatalog.getCatalogAlbumTracks(
132
- service: self.catalogService,
133
- albumId: albumId,
134
- options: options as NSDictionary
135
- )
136
- }
137
-
138
- AsyncFunction("getCatalogArtistAlbums") { (artistId: String, options: [String: Any]) -> [String: Any] in
139
- try await ExpoBridgeCatalog.getCatalogArtistAlbums(
140
- service: self.catalogService,
141
- artistId: artistId,
142
- options: options as NSDictionary
143
- )
144
- }
145
-
146
- AsyncFunction("getCatalogPlaylistTracks") { (playlistId: String, options: [String: Any]) -> [String: Any] in
147
- try await ExpoBridgeCatalog.getCatalogPlaylistTracks(
148
- service: self.catalogService,
149
- playlistId: playlistId,
150
- options: options as NSDictionary
151
- )
152
- }
153
-
154
- AsyncFunction("getCatalogCharts") { (types: [String], options: [String: Any]) -> [String: Any] in
155
- try await ExpoBridgeCatalog.getCatalogCharts(
156
- service: self.catalogService,
157
- types: types,
158
- options: options
159
- )
160
- }
161
-
162
- AsyncFunction("getCatalogResources") { (type: String, ids: [String]) -> [String: Any] in
163
- try await ExpoBridgeCatalog.getCatalogResources(
164
- service: self.catalogService,
165
- type: type,
166
- ids: ids
167
- )
168
- }
169
-
170
- // MARK: - Library
171
-
172
- AsyncFunction("getUserPlaylists") { (musicUserToken: String, options: [String: Any]) -> [String: Any] in
173
- try await ExpoBridgeLibrary.getUserPlaylists(
174
- service: self.libraryService,
175
- musicUserToken: musicUserToken,
176
- options: options as NSDictionary
177
- )
178
- }
179
-
180
- AsyncFunction("getLibrarySongs") { (musicUserToken: String, options: [String: Any]) -> [String: Any] in
181
- try await ExpoBridgeLibrary.getLibrarySongs(
182
- service: self.libraryService,
183
- musicUserToken: musicUserToken,
184
- options: options as NSDictionary
185
- )
186
- }
187
-
188
- AsyncFunction("getPlaylistSongs") { (musicUserToken: String, playlistId: String, options: [String: Any]) -> [String: Any] in
189
- try await ExpoBridgeLibrary.getPlaylistSongs(
190
- service: self.libraryService,
191
- musicUserToken: musicUserToken,
192
- playlistId: playlistId
193
- )
194
- }
195
-
196
- AsyncFunction("getLibraryArtists") { (musicUserToken: String, options: [String: Any]) -> [String: Any] in
197
- try await ExpoBridgeLibrary.getLibraryArtists(
198
- service: self.libraryService,
199
- musicUserToken: musicUserToken,
200
- options: options as NSDictionary
201
- )
202
- }
203
-
204
- AsyncFunction("getLibraryAlbums") { (musicUserToken: String, options: [String: Any]) -> [String: Any] in
205
- try await ExpoBridgeLibrary.getLibraryAlbums(
206
- service: self.libraryService,
207
- musicUserToken: musicUserToken,
208
- options: options as NSDictionary
209
- )
210
- }
211
-
212
- AsyncFunction("getLibraryMusicVideos") { (musicUserToken: String, options: [String: Any]) -> [String: Any] in
213
- try await ExpoBridgeLibrary.getLibraryMusicVideos(
214
- service: self.libraryService,
215
- musicUserToken: musicUserToken,
216
- options: options as NSDictionary
217
- )
218
- }
219
-
220
- AsyncFunction("librarySearch") { (musicUserToken: String, term: String, types: [String], options: [String: Any]) -> [String: Any] in
221
- try await ExpoBridgeLibrary.librarySearch(
222
- service: self.libraryService,
223
- musicUserToken: musicUserToken,
224
- term: term,
225
- types: types,
226
- options: options as NSDictionary
227
- )
228
- }
229
-
230
- // MARK: - History
231
-
232
- AsyncFunction("getRecentlyPlayedResources") { (musicUserToken: String) -> [String: Any] in
233
- try await ExpoBridgeHistory.getRecentlyPlayedResources(
234
- service: self.historyService,
235
- musicUserToken: musicUserToken)
236
- }
237
-
238
- AsyncFunction("getRecentlyPlayedTracks") { (musicUserToken: String, options: [String: Any]) -> [String: Any] in
239
- try await ExpoBridgeHistory.getRecentlyPlayedTracks(
240
- service: self.historyService,
241
- musicUserToken: musicUserToken,
242
- options: options as NSDictionary
243
- )
244
- }
245
-
246
- AsyncFunction("getHeavyRotation") { (musicUserToken: String, options: [String: Any]) -> [String: Any] in
247
- try await ExpoBridgeHistory.getHeavyRotation(
248
- service: self.historyService,
249
- musicUserToken: musicUserToken,
250
- options: options as NSDictionary
251
- )
252
- }
253
-
254
- AsyncFunction("getRecentlyPlayedStations") { (musicUserToken: String, options: [String: Any]) -> [String: Any] in
255
- try await ExpoBridgeHistory.getRecentlyPlayedStations(
256
- service: self.historyService,
257
- musicUserToken: musicUserToken,
258
- options: options as NSDictionary
259
- )
260
- }
261
-
262
- AsyncFunction("getRecentlyAdded") { (musicUserToken: String, options: [String: Any]) -> [String: Any] in
263
- try await ExpoBridgeHistory.getRecentlyAdded(
264
- service: self.historyService,
265
- musicUserToken: musicUserToken,
266
- options: options as NSDictionary
267
- )
268
- }
269
-
270
- // MARK: - Player
271
-
272
- AsyncFunction("setPlaybackQueue") { (itemId: String, type: String) -> String in
273
- try await AppleMusicBridgeError.rethrow {
274
- try await self.queueService.setQueue(itemId: itemId, type: type)
275
- return "Track(s) added to queue"
276
- }
277
- }
278
-
279
- AsyncFunction("configurePlayer") { (mixWithOthers: Bool) -> [String: Any] in
280
- try self.playbackController.configureAudioSession(mixWithOthers: mixWithOthers)
281
- return BridgeResponses.configurePlayer(mixWithOthers: mixWithOthers)
282
- }
283
-
284
- Function("play") {
285
- Task {
286
- do {
287
- try await self.playbackController.play()
288
- } catch {
289
- self.emitPlaybackError(error, operation: "play")
290
- }
291
- }
292
- }
293
-
294
- Function("pause") {
295
- self.playbackController.pause()
296
- }
297
-
298
- Function("skipToNextEntry") {
299
- Task {
300
- do {
301
- try await self.playbackController.skipToNext()
302
- } catch {
303
- self.emitPlaybackError(error, operation: "skipToNext")
304
- }
305
- }
306
- }
307
-
308
- Function("skipToPreviousEntry") {
309
- Task {
310
- do {
311
- try await self.playbackController.skipToPrevious()
312
- } catch {
313
- self.emitPlaybackError(error, operation: "skipToPrevious")
314
- }
315
- }
316
- }
317
-
318
- Function("restartCurrentEntry") {
319
- Task { @MainActor in
320
- self.playbackController.restartCurrentEntry()
321
- self.playbackTimeDidUpdate(0)
322
- }
323
- }
324
-
325
- Function("seekToTime") { (time: Double) in
326
- Task { @MainActor in
327
- self.playbackController.seek(to: time)
328
- self.playbackTimeDidUpdate(time)
329
- }
330
- }
331
-
332
- Function("togglePlayerState") {
333
- Task {
334
- do {
335
- try await self.playbackController.togglePlayback()
336
- } catch {
337
- self.emitPlaybackError(error, operation: "togglePlayback")
338
- }
339
- }
340
- }
341
-
342
- AsyncFunction("getCurrentState") { () -> [String: Any] in
343
- let state = self.playbackController.state
344
- let songInfo = await self.playbackController.fetchCurrentSongInfo()
345
-
346
- var result: [String: Any] = [
347
- "playbackRate": state.playbackRate,
348
- "playbackStatus": MusicItemMapper.describePlaybackStatus(state.playbackStatus),
349
- "playbackTime": self.playbackController.playbackTime,
350
- ]
351
- if let songInfo = songInfo {
352
- result["currentSong"] = songInfo
353
- }
354
- return result
355
- }
356
-
357
- AsyncFunction("playLibrarySong") { (musicUserToken: String, songId: String) -> String in
358
- try await AppleMusicBridgeError.rethrow {
359
- try await self.queueService.playLibrarySong(musicUserToken: musicUserToken, songId: songId)
360
- return "Library song added to queue"
361
- }
362
- }
363
-
364
- AsyncFunction("playLibraryPlaylist") { (musicUserToken: String, playlistId: String, startingAt: Int) -> String in
365
- try await AppleMusicBridgeError.rethrow {
366
- try await self.queueService.playLibraryPlaylist(
367
- musicUserToken: musicUserToken,
368
- playlistId: playlistId,
369
- startingAt: startingAt
370
- )
371
- return "Library playlist added to queue"
372
- }
373
- }
374
-
375
- // MARK: - Ratings
376
-
377
- AsyncFunction("getRating") { (musicUserToken: String, resourceType: String, id: String) -> [String: Any]? in
378
- try await AppleMusicBridgeError.rethrow {
379
- try await self.ratingsService.getRating(
380
- musicUserToken: musicUserToken, resourceType: resourceType, id: id)
381
- }
382
- }
383
-
384
- AsyncFunction("setRating") { (musicUserToken: String, resourceType: String, id: String, value: Int) -> [String: Any] in
385
- try await AppleMusicBridgeError.rethrow {
386
- try await self.ratingsService.setRating(
387
- musicUserToken: musicUserToken, resourceType: resourceType, id: id, value: value)
388
- }
389
- }
390
-
391
- AsyncFunction("clearRating") { (musicUserToken: String, resourceType: String, id: String) -> Void in
392
- try await AppleMusicBridgeError.rethrow {
393
- try await self.ratingsService.clearRating(
394
- musicUserToken: musicUserToken, resourceType: resourceType, id: id)
395
- }
396
- }
397
-
398
- AsyncFunction("addToFavorites") { (musicUserToken: String, resourceIds: [String: [String]]) -> Void in
399
- try await AppleMusicBridgeError.rethrow {
400
- try await self.ratingsService.addToFavorites(
401
- musicUserToken: musicUserToken, resourceIds: resourceIds)
402
- }
403
- }
404
-
405
- AsyncFunction("removeFromFavorites") { (musicUserToken: String, resourceIds: [String: [String]]) -> Void in
406
- try await AppleMusicBridgeError.rethrow {
407
- try await self.ratingsService.removeFromFavorites(
408
- musicUserToken: musicUserToken, resourceIds: resourceIds)
409
- }
410
- }
411
-
412
- // MARK: - Library mutations
413
-
414
- AsyncFunction("addToLibrary") { (musicUserToken: String, resourceIds: [String: [String]]) -> Void in
415
- try await AppleMusicBridgeError.rethrow {
416
- try await self.libraryMutationsService.addToLibrary(
417
- musicUserToken: musicUserToken, resourceIds: resourceIds)
418
- }
419
- }
420
-
421
- AsyncFunction("createLibraryPlaylist") { (musicUserToken: String, options: [String: Any]) -> [String: Any] in
422
- try await AppleMusicBridgeError.rethrow {
423
- let name = options["name"] as? String ?? ""
424
- let description = options["description"] as? String
425
- let isPublic = options["isPublic"] as? Bool ?? false
426
- let tracks = options["tracks"] as? [[String: String]]
427
- return try await self.libraryMutationsService.createPlaylist(
428
- musicUserToken: musicUserToken,
429
- name: name,
430
- description: description,
431
- isPublic: isPublic,
432
- tracks: tracks
433
- )
434
- }
435
- }
436
-
437
- AsyncFunction("addTracksToLibraryPlaylist") { (musicUserToken: String, playlistId: String, tracks: [[String: String]]) -> Void in
438
- try await AppleMusicBridgeError.rethrow {
439
- try await self.libraryMutationsService.addTracksToPlaylist(
440
- musicUserToken: musicUserToken,
441
- playlistId: playlistId,
442
- tracks: tracks
443
- )
444
- }
445
- }
446
-
447
- // MARK: - Recommendations
448
-
449
- AsyncFunction("getRecommendations") { (musicUserToken: String, ids: [String]?) -> [String: Any] in
450
- try await ExpoBridgeRecommendations.getRecommendations(
451
- service: self.recommendationsService,
452
- musicUserToken: musicUserToken,
453
- ids: ids
454
- )
455
- }
456
-
457
- AsyncFunction("getReplay") { (musicUserToken: String, year: Int?) -> [String: Any] in
458
- try await ExpoBridgeRecommendations.getReplay(
459
- service: self.recommendationsService,
460
- musicUserToken: musicUserToken,
461
- year: year
462
- )
463
- }
464
- }
465
-
466
- private func emitPlaybackError(_ error: Error, operation: String) {
20
+ func emitPlaybackError(_ error: Error, operation: String) {
467
21
  let nsError = error as NSError
468
22
  sendEvent(
469
23
  "onPlaybackError",