@wwdrew/expo-apple-music 1.0.0 → 1.1.1
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 +29 -7
- package/android/src/main/java/expo/modules/applemusic/ExpoAppleMusicModule.kt +14 -8
- package/ios/ExpoAppleMusicModule.swift +11 -457
- package/ios/ExpoAppleMusicModuleDefinition.swift +411 -0
- package/ios/LibraryService.swift +3 -126
- package/ios/PlaybackObserver.swift +4 -4
- package/ios/bridge/ExpoBridgeAuth.swift +45 -0
- package/ios/bridge/ExpoBridgeCatalog.swift +36 -0
- package/ios/bridge/ExpoBridgeLibraryMutations.swift +49 -0
- package/ios/bridge/ExpoBridgePlayer.swift +55 -0
- package/ios/bridge/ExpoBridgeRatings.swift +60 -0
- package/package.json +2 -3
- package/plugin/build/apple-music-aars.d.ts +14 -0
- package/plugin/build/apple-music-aars.js +58 -0
- package/plugin/build/with-expo-apple-music.d.ts +8 -0
- package/plugin/build/with-expo-apple-music.js +30 -1
- package/android/libs/mediaplayback-release-1.1.1.aar +0 -0
- package/android/libs/musickitauth-release-1.1.2.aar +0 -0
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.
|
|
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({
|
|
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 `
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
18
|
+
var playbackObserver: PlaybackObserver?
|
|
19
19
|
|
|
20
|
-
|
|
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",
|