@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.
- package/ATTRIBUTION.md +24 -0
- package/LICENSE +190 -0
- package/NOTICE +7 -0
- package/README.md +81 -0
- package/android/build.gradle +26 -0
- package/android/libs/mediaplayback-release-1.1.1.aar +0 -0
- package/android/libs/musickitauth-release-1.1.2.aar +0 -0
- package/android/src/main/AndroidManifest.xml +16 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidCatalogService.kt +86 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidDeveloperToken.kt +39 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidHistoryService.kt +24 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidLibraryMutationsService.kt +30 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidLibraryService.kt +61 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidPlaybackController.kt +484 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidPlaybackObserver.kt +173 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidQueueService.kt +78 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidRatingsService.kt +27 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidRecommendationsService.kt +15 -0
- package/android/src/main/java/expo/modules/applemusic/AndroidSubscriptionService.kt +24 -0
- package/android/src/main/java/expo/modules/applemusic/AppleMusicErrorCodes.kt +13 -0
- package/android/src/main/java/expo/modules/applemusic/AppleMusicErrors.kt +46 -0
- package/android/src/main/java/expo/modules/applemusic/AppleMusicHttpMethod.kt +8 -0
- package/android/src/main/java/expo/modules/applemusic/AppleMusicJsonMapper.kt +258 -0
- package/android/src/main/java/expo/modules/applemusic/AppleMusicNativeLoader.kt +32 -0
- package/android/src/main/java/expo/modules/applemusic/AppleMusicRestJson.kt +40 -0
- package/android/src/main/java/expo/modules/applemusic/AppleMusicRestQuery.kt +12 -0
- package/android/src/main/java/expo/modules/applemusic/AppleMusicRestStack.kt +19 -0
- package/android/src/main/java/expo/modules/applemusic/AppleMusicRestTransport.kt +118 -0
- package/android/src/main/java/expo/modules/applemusic/AuthenticatedSession.kt +57 -0
- package/android/src/main/java/expo/modules/applemusic/BridgeResponses.kt +55 -0
- package/android/src/main/java/expo/modules/applemusic/CatalogRestClient.kt +306 -0
- package/android/src/main/java/expo/modules/applemusic/ExpoAppleMusicModule.kt +152 -0
- package/android/src/main/java/expo/modules/applemusic/HistoryRestClient.kt +60 -0
- package/android/src/main/java/expo/modules/applemusic/LibraryIds.kt +6 -0
- package/android/src/main/java/expo/modules/applemusic/LibraryMutationsRestClient.kt +95 -0
- package/android/src/main/java/expo/modules/applemusic/LibraryRestClient.kt +195 -0
- package/android/src/main/java/expo/modules/applemusic/MusicKitAuthContract.kt +78 -0
- package/android/src/main/java/expo/modules/applemusic/MusicKitAuthStorage.kt +76 -0
- package/android/src/main/java/expo/modules/applemusic/MusicKitTokenProvider.kt +13 -0
- package/android/src/main/java/expo/modules/applemusic/PaginationOptions.kt +14 -0
- package/android/src/main/java/expo/modules/applemusic/RatingsRestClient.kt +72 -0
- package/android/src/main/java/expo/modules/applemusic/RecommendationsRestClient.kt +37 -0
- package/android/src/main/java/expo/modules/applemusic/StorefrontRestClient.kt +44 -0
- package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeAuth.kt +69 -0
- package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeCatalog.kt +76 -0
- package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeHistory.kt +35 -0
- package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeLibrary.kt +54 -0
- package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeLibraryMutations.kt +30 -0
- package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgePlayer.kt +89 -0
- package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeRatings.kt +29 -0
- package/android/src/main/java/expo/modules/applemusic/bridge/ExpoBridgeRecommendations.kt +18 -0
- package/app.plugin.js +1 -0
- package/build/ExpoAppleMusicModule.web.d.ts +15 -0
- package/build/ExpoAppleMusicModule.web.d.ts.map +1 -0
- package/build/ExpoAppleMusicModule.web.js +33 -0
- package/build/ExpoAppleMusicModule.web.js.map +1 -0
- package/build/api/call-native.d.ts +6 -0
- package/build/api/call-native.d.ts.map +1 -0
- package/build/api/call-native.js +35 -0
- package/build/api/call-native.js.map +1 -0
- package/build/api/decode-jwt-exp.d.ts +5 -0
- package/build/api/decode-jwt-exp.d.ts.map +1 -0
- package/build/api/decode-jwt-exp.js +28 -0
- package/build/api/decode-jwt-exp.js.map +1 -0
- package/build/api/library-ids.d.ts +4 -0
- package/build/api/library-ids.d.ts.map +1 -0
- package/build/api/library-ids.js +11 -0
- package/build/api/library-ids.js.map +1 -0
- package/build/api/pagination.d.ts +12 -0
- package/build/api/pagination.d.ts.map +1 -0
- package/build/api/pagination.js +13 -0
- package/build/api/pagination.js.map +1 -0
- package/build/api/parse-authorize-result.d.ts +3 -0
- package/build/api/parse-authorize-result.d.ts.map +1 -0
- package/build/api/parse-authorize-result.js +27 -0
- package/build/api/parse-authorize-result.js.map +1 -0
- package/build/api/require-music-user-token.d.ts +2 -0
- package/build/api/require-music-user-token.d.ts.map +1 -0
- package/build/api/require-music-user-token.js +14 -0
- package/build/api/require-music-user-token.js.map +1 -0
- package/build/api/sync-developer-token.d.ts +4 -0
- package/build/api/sync-developer-token.d.ts.map +1 -0
- package/build/api/sync-developer-token.js +27 -0
- package/build/api/sync-developer-token.js.map +1 -0
- package/build/bridge/bridge-methods.d.ts +17 -0
- package/build/bridge/bridge-methods.d.ts.map +1 -0
- package/build/bridge/bridge-methods.js +71 -0
- package/build/bridge/bridge-methods.js.map +1 -0
- package/build/bridge/bridge-responses.d.ts +64 -0
- package/build/bridge/bridge-responses.d.ts.map +1 -0
- package/build/bridge/bridge-responses.js +67 -0
- package/build/bridge/bridge-responses.js.map +1 -0
- package/build/bridge/handlers/auth-bridge.d.ts +11 -0
- package/build/bridge/handlers/auth-bridge.d.ts.map +1 -0
- package/build/bridge/handlers/auth-bridge.js +49 -0
- package/build/bridge/handlers/auth-bridge.js.map +1 -0
- package/build/bridge/handlers/catalog-bridge.d.ts +34 -0
- package/build/bridge/handlers/catalog-bridge.d.ts.map +1 -0
- package/build/bridge/handlers/catalog-bridge.js +58 -0
- package/build/bridge/handlers/catalog-bridge.js.map +1 -0
- package/build/bridge/handlers/history-bridge.d.ts +19 -0
- package/build/bridge/handlers/history-bridge.d.ts.map +1 -0
- package/build/bridge/handlers/history-bridge.js +31 -0
- package/build/bridge/handlers/history-bridge.js.map +1 -0
- package/build/bridge/handlers/index.d.ts +124 -0
- package/build/bridge/handlers/index.d.ts.map +1 -0
- package/build/bridge/handlers/index.js +21 -0
- package/build/bridge/handlers/index.js.map +1 -0
- package/build/bridge/handlers/library-bridge.d.ts +23 -0
- package/build/bridge/handlers/library-bridge.d.ts.map +1 -0
- package/build/bridge/handlers/library-bridge.js +41 -0
- package/build/bridge/handlers/library-bridge.js.map +1 -0
- package/build/bridge/handlers/library-mutations-bridge.d.ts +16 -0
- package/build/bridge/handlers/library-mutations-bridge.d.ts.map +1 -0
- package/build/bridge/handlers/library-mutations-bridge.js +20 -0
- package/build/bridge/handlers/library-mutations-bridge.js.map +1 -0
- package/build/bridge/handlers/player-bridge.d.ts +18 -0
- package/build/bridge/handlers/player-bridge.d.ts.map +1 -0
- package/build/bridge/handlers/player-bridge.js +42 -0
- package/build/bridge/handlers/player-bridge.js.map +1 -0
- package/build/bridge/handlers/ratings-bridge.d.ts +15 -0
- package/build/bridge/handlers/ratings-bridge.d.ts.map +1 -0
- package/build/bridge/handlers/ratings-bridge.js +16 -0
- package/build/bridge/handlers/ratings-bridge.js.map +1 -0
- package/build/bridge/handlers/recommendations-bridge.d.ts +10 -0
- package/build/bridge/handlers/recommendations-bridge.d.ts.map +1 -0
- package/build/bridge/handlers/recommendations-bridge.js +14 -0
- package/build/bridge/handlers/recommendations-bridge.js.map +1 -0
- package/build/constants/apple-music-error-codes.d.ts +22 -0
- package/build/constants/apple-music-error-codes.d.ts.map +1 -0
- package/build/constants/apple-music-error-codes.js +21 -0
- package/build/constants/apple-music-error-codes.js.map +1 -0
- package/build/hooks/use-current-song.d.ts +10 -0
- package/build/hooks/use-current-song.d.ts.map +1 -0
- package/build/hooks/use-current-song.js +35 -0
- package/build/hooks/use-current-song.js.map +1 -0
- package/build/hooks/use-is-playing.d.ts +6 -0
- package/build/hooks/use-is-playing.d.ts.map +1 -0
- package/build/hooks/use-is-playing.js +21 -0
- package/build/hooks/use-is-playing.js.map +1 -0
- package/build/hooks/use-playback-state.d.ts +12 -0
- package/build/hooks/use-playback-state.d.ts.map +1 -0
- package/build/hooks/use-playback-state.js +41 -0
- package/build/hooks/use-playback-state.js.map +1 -0
- package/build/index.d.ts +52 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +44 -0
- package/build/index.js.map +1 -0
- package/build/mappers/apple-music-json-mapper.d.ts +94 -0
- package/build/mappers/apple-music-json-mapper.d.ts.map +1 -0
- package/build/mappers/apple-music-json-mapper.js +212 -0
- package/build/mappers/apple-music-json-mapper.js.map +1 -0
- package/build/modules/auth.d.ts +32 -0
- package/build/modules/auth.d.ts.map +1 -0
- package/build/modules/auth.js +60 -0
- package/build/modules/auth.js.map +1 -0
- package/build/modules/catalog.d.ts +46 -0
- package/build/modules/catalog.d.ts.map +1 -0
- package/build/modules/catalog.js +47 -0
- package/build/modules/catalog.js.map +1 -0
- package/build/modules/history.d.ts +17 -0
- package/build/modules/history.d.ts.map +1 -0
- package/build/modules/history.js +28 -0
- package/build/modules/history.js.map +1 -0
- package/build/modules/library-mutations.d.ts +9 -0
- package/build/modules/library-mutations.d.ts.map +1 -0
- package/build/modules/library-mutations.js +41 -0
- package/build/modules/library-mutations.js.map +1 -0
- package/build/modules/library.d.ts +21 -0
- package/build/modules/library.d.ts.map +1 -0
- package/build/modules/library.js +38 -0
- package/build/modules/library.js.map +1 -0
- package/build/modules/player.d.ts +53 -0
- package/build/modules/player.d.ts.map +1 -0
- package/build/modules/player.js +68 -0
- package/build/modules/player.js.map +1 -0
- package/build/modules/ratings.d.ts +10 -0
- package/build/modules/ratings.d.ts.map +1 -0
- package/build/modules/ratings.js +37 -0
- package/build/modules/ratings.js.map +1 -0
- package/build/modules/recommendations.d.ts +7 -0
- package/build/modules/recommendations.d.ts.map +1 -0
- package/build/modules/recommendations.js +15 -0
- package/build/modules/recommendations.js.map +1 -0
- package/build/native-module.d.ts +5 -0
- package/build/native-module.d.ts.map +1 -0
- package/build/native-module.js +5 -0
- package/build/native-module.js.map +1 -0
- package/build/native-module.web.d.ts +11 -0
- package/build/native-module.web.d.ts.map +1 -0
- package/build/native-module.web.js +11 -0
- package/build/native-module.web.js.map +1 -0
- package/build/rest/apple-music-rest-stack.d.ts +21 -0
- package/build/rest/apple-music-rest-stack.d.ts.map +1 -0
- package/build/rest/apple-music-rest-stack.js +21 -0
- package/build/rest/apple-music-rest-stack.js.map +1 -0
- package/build/rest/apple-music-rest-transport.d.ts +8 -0
- package/build/rest/apple-music-rest-transport.d.ts.map +1 -0
- package/build/rest/apple-music-rest-transport.js +2 -0
- package/build/rest/apple-music-rest-transport.js.map +1 -0
- package/build/rest/catalog-rest-client.d.ts +38 -0
- package/build/rest/catalog-rest-client.d.ts.map +1 -0
- package/build/rest/catalog-rest-client.js +189 -0
- package/build/rest/catalog-rest-client.js.map +1 -0
- package/build/rest/history-rest-client.d.ts +37 -0
- package/build/rest/history-rest-client.d.ts.map +1 -0
- package/build/rest/history-rest-client.js +30 -0
- package/build/rest/history-rest-client.js.map +1 -0
- package/build/rest/library-ids.d.ts +2 -0
- package/build/rest/library-ids.d.ts.map +1 -0
- package/build/rest/library-ids.js +2 -0
- package/build/rest/library-ids.js.map +1 -0
- package/build/rest/library-mutations-rest-client.d.ts +22 -0
- package/build/rest/library-mutations-rest-client.d.ts.map +1 -0
- package/build/rest/library-mutations-rest-client.js +37 -0
- package/build/rest/library-mutations-rest-client.js.map +1 -0
- package/build/rest/library-rest-client.d.ts +59 -0
- package/build/rest/library-rest-client.d.ts.map +1 -0
- package/build/rest/library-rest-client.js +141 -0
- package/build/rest/library-rest-client.js.map +1 -0
- package/build/rest/ratings-rest-client.d.ts +18 -0
- package/build/rest/ratings-rest-client.d.ts.map +1 -0
- package/build/rest/ratings-rest-client.js +37 -0
- package/build/rest/ratings-rest-client.js.map +1 -0
- package/build/rest/recommendations-rest-client.d.ts +32 -0
- package/build/rest/recommendations-rest-client.d.ts.map +1 -0
- package/build/rest/recommendations-rest-client.js +26 -0
- package/build/rest/recommendations-rest-client.js.map +1 -0
- package/build/rest/resource-ids-query.d.ts +2 -0
- package/build/rest/resource-ids-query.d.ts.map +1 -0
- package/build/rest/resource-ids-query.js +10 -0
- package/build/rest/resource-ids-query.js.map +1 -0
- package/build/rest/rest-json.d.ts +11 -0
- package/build/rest/rest-json.d.ts.map +1 -0
- package/build/rest/rest-json.js +29 -0
- package/build/rest/rest-json.js.map +1 -0
- package/build/rest/storefront-rest-client.d.ts +14 -0
- package/build/rest/storefront-rest-client.d.ts.map +1 -0
- package/build/rest/storefront-rest-client.js +36 -0
- package/build/rest/storefront-rest-client.js.map +1 -0
- package/build/types/album.d.ts +8 -0
- package/build/types/album.d.ts.map +1 -0
- package/build/types/album.js +2 -0
- package/build/types/album.js.map +1 -0
- package/build/types/albums-response.d.ts +5 -0
- package/build/types/albums-response.d.ts.map +1 -0
- package/build/types/albums-response.js +2 -0
- package/build/types/albums-response.js.map +1 -0
- package/build/types/android-authorize-options.d.ts +18 -0
- package/build/types/android-authorize-options.d.ts.map +1 -0
- package/build/types/android-authorize-options.js +2 -0
- package/build/types/android-authorize-options.js.map +1 -0
- package/build/types/artist.d.ts +9 -0
- package/build/types/artist.d.ts.map +1 -0
- package/build/types/artist.js +2 -0
- package/build/types/artist.js.map +1 -0
- package/build/types/auth-status.d.ts +19 -0
- package/build/types/auth-status.d.ts.map +1 -0
- package/build/types/auth-status.js +18 -0
- package/build/types/auth-status.js.map +1 -0
- package/build/types/authorize-result.d.ts +8 -0
- package/build/types/authorize-result.d.ts.map +1 -0
- package/build/types/authorize-result.js +2 -0
- package/build/types/authorize-result.js.map +1 -0
- package/build/types/catalog-album-tracks.d.ts +5 -0
- package/build/types/catalog-album-tracks.d.ts.map +1 -0
- package/build/types/catalog-album-tracks.js +2 -0
- package/build/types/catalog-album-tracks.js.map +1 -0
- package/build/types/catalog-charts.d.ts +25 -0
- package/build/types/catalog-charts.d.ts.map +1 -0
- package/build/types/catalog-charts.js +7 -0
- package/build/types/catalog-charts.js.map +1 -0
- package/build/types/catalog-resource-type.d.ts +11 -0
- package/build/types/catalog-resource-type.d.ts.map +1 -0
- package/build/types/catalog-resource-type.js +10 -0
- package/build/types/catalog-resource-type.js.map +1 -0
- package/build/types/catalog-search.d.ts +24 -0
- package/build/types/catalog-search.d.ts.map +1 -0
- package/build/types/catalog-search.js +9 -0
- package/build/types/catalog-search.js.map +1 -0
- package/build/types/check-subscription.d.ts +31 -0
- package/build/types/check-subscription.d.ts.map +1 -0
- package/build/types/check-subscription.js +5 -0
- package/build/types/check-subscription.js.map +1 -0
- package/build/types/library-music-videos.d.ts +5 -0
- package/build/types/library-music-videos.d.ts.map +1 -0
- package/build/types/library-music-videos.js +2 -0
- package/build/types/library-music-videos.js.map +1 -0
- package/build/types/library-mutations.d.ts +21 -0
- package/build/types/library-mutations.d.ts.map +1 -0
- package/build/types/library-mutations.js +8 -0
- package/build/types/library-mutations.js.map +1 -0
- package/build/types/library-search.d.ts +22 -0
- package/build/types/library-search.d.ts.map +1 -0
- package/build/types/library-search.js +9 -0
- package/build/types/library-search.js.map +1 -0
- package/build/types/music-item.d.ts +8 -0
- package/build/types/music-item.d.ts.map +1 -0
- package/build/types/music-item.js +7 -0
- package/build/types/music-item.js.map +1 -0
- package/build/types/music-video.d.ts +8 -0
- package/build/types/music-video.d.ts.map +1 -0
- package/build/types/music-video.js +2 -0
- package/build/types/music-video.js.map +1 -0
- package/build/types/pagination.d.ts +11 -0
- package/build/types/pagination.d.ts.map +1 -0
- package/build/types/pagination.js +2 -0
- package/build/types/pagination.js.map +1 -0
- package/build/types/playback-state.d.ts +9 -0
- package/build/types/playback-state.d.ts.map +1 -0
- package/build/types/playback-state.js +2 -0
- package/build/types/playback-state.js.map +1 -0
- package/build/types/playback-status.d.ts +10 -0
- package/build/types/playback-status.d.ts.map +1 -0
- package/build/types/playback-status.js +9 -0
- package/build/types/playback-status.js.map +1 -0
- package/build/types/playlist.d.ts +15 -0
- package/build/types/playlist.d.ts.map +1 -0
- package/build/types/playlist.js +2 -0
- package/build/types/playlist.js.map +1 -0
- package/build/types/rating.d.ts +34 -0
- package/build/types/rating.d.ts.map +1 -0
- package/build/types/rating.js +26 -0
- package/build/types/rating.js.map +1 -0
- package/build/types/recent-resource.d.ts +11 -0
- package/build/types/recent-resource.d.ts.map +1 -0
- package/build/types/recent-resource.js +2 -0
- package/build/types/recent-resource.js.map +1 -0
- package/build/types/recommendation.d.ts +37 -0
- package/build/types/recommendation.d.ts.map +1 -0
- package/build/types/recommendation.js +2 -0
- package/build/types/recommendation.js.map +1 -0
- package/build/types/song.d.ts +8 -0
- package/build/types/song.d.ts.map +1 -0
- package/build/types/song.js +2 -0
- package/build/types/song.js.map +1 -0
- package/build/types/station.d.ts +9 -0
- package/build/types/station.d.ts.map +1 -0
- package/build/types/station.js +2 -0
- package/build/types/station.js.map +1 -0
- package/build/types/storefront.d.ts +4 -0
- package/build/types/storefront.d.ts.map +1 -0
- package/build/types/storefront.js +2 -0
- package/build/types/storefront.js.map +1 -0
- package/build/types/tracks-from-library.d.ts +11 -0
- package/build/types/tracks-from-library.d.ts.map +1 -0
- package/build/types/tracks-from-library.js +2 -0
- package/build/types/tracks-from-library.js.map +1 -0
- package/build/utils/apple-music-error.d.ts +10 -0
- package/build/utils/apple-music-error.d.ts.map +1 -0
- package/build/utils/apple-music-error.js +13 -0
- package/build/utils/apple-music-error.js.map +1 -0
- package/build/utils/get-error-message.d.ts +2 -0
- package/build/utils/get-error-message.d.ts.map +1 -0
- package/build/utils/get-error-message.js +21 -0
- package/build/utils/get-error-message.js.map +1 -0
- package/build/utils/is-library-item.d.ts +2 -0
- package/build/utils/is-library-item.d.ts.map +1 -0
- package/build/utils/is-library-item.js +4 -0
- package/build/utils/is-library-item.js.map +1 -0
- package/build/utils/normalize-resource-ids.d.ts +4 -0
- package/build/utils/normalize-resource-ids.d.ts.map +1 -0
- package/build/utils/normalize-resource-ids.js +12 -0
- package/build/utils/normalize-resource-ids.js.map +1 -0
- package/build/web/MusicKitLoader.d.ts +11 -0
- package/build/web/MusicKitLoader.d.ts.map +1 -0
- package/build/web/MusicKitLoader.js +135 -0
- package/build/web/MusicKitLoader.js.map +1 -0
- package/build/web/WebAppleMusicApiClient.d.ts +151 -0
- package/build/web/WebAppleMusicApiClient.d.ts.map +1 -0
- package/build/web/WebAppleMusicApiClient.js +139 -0
- package/build/web/WebAppleMusicApiClient.js.map +1 -0
- package/build/web/WebAppleMusicRestTransport.d.ts +9 -0
- package/build/web/WebAppleMusicRestTransport.d.ts.map +1 -0
- package/build/web/WebAppleMusicRestTransport.js +31 -0
- package/build/web/WebAppleMusicRestTransport.js.map +1 -0
- package/build/web/WebPlaybackController.d.ts +12 -0
- package/build/web/WebPlaybackController.d.ts.map +1 -0
- package/build/web/WebPlaybackController.js +90 -0
- package/build/web/WebPlaybackController.js.map +1 -0
- package/build/web/WebPlaybackObserver.d.ts +22 -0
- package/build/web/WebPlaybackObserver.d.ts.map +1 -0
- package/build/web/WebPlaybackObserver.js +106 -0
- package/build/web/WebPlaybackObserver.js.map +1 -0
- package/build/web/WebQueueService.d.ts +10 -0
- package/build/web/WebQueueService.d.ts.map +1 -0
- package/build/web/WebQueueService.js +53 -0
- package/build/web/WebQueueService.js.map +1 -0
- package/build/web/WebSubscriptionService.d.ts +7 -0
- package/build/web/WebSubscriptionService.d.ts.map +1 -0
- package/build/web/WebSubscriptionService.js +24 -0
- package/build/web/WebSubscriptionService.js.map +1 -0
- package/build/web/apple-music-errors.d.ts +11 -0
- package/build/web/apple-music-errors.d.ts.map +1 -0
- package/build/web/apple-music-errors.js +31 -0
- package/build/web/apple-music-errors.js.map +1 -0
- package/build/web/extract-music-user-token.d.ts +4 -0
- package/build/web/extract-music-user-token.d.ts.map +1 -0
- package/build/web/extract-music-user-token.js +15 -0
- package/build/web/extract-music-user-token.js.map +1 -0
- package/build/web/map-auth-status.d.ts +17 -0
- package/build/web/map-auth-status.d.ts.map +1 -0
- package/build/web/map-auth-status.js +85 -0
- package/build/web/map-auth-status.js.map +1 -0
- package/build/web/music-kit-api.d.ts +18 -0
- package/build/web/music-kit-api.d.ts.map +1 -0
- package/build/web/music-kit-api.js +120 -0
- package/build/web/music-kit-api.js.map +1 -0
- package/build/web/musickit-types.d.ts +70 -0
- package/build/web/musickit-types.d.ts.map +1 -0
- package/build/web/musickit-types.js +3 -0
- package/build/web/musickit-types.js.map +1 -0
- package/build/web/pagination.d.ts +2 -0
- package/build/web/pagination.d.ts.map +1 -0
- package/build/web/pagination.js +2 -0
- package/build/web/pagination.js.map +1 -0
- package/expo-module.config.json +19 -0
- package/ios/AppleMusicBridgeError.swift +60 -0
- package/ios/AppleMusicErrorCodes.swift +11 -0
- package/ios/AppleMusicRestClient.swift +213 -0
- package/ios/AuthenticatedSession.swift +64 -0
- package/ios/BridgePagination.swift +17 -0
- package/ios/BridgeResponses.swift +82 -0
- package/ios/CatalogSearchStore.swift +13 -0
- package/ios/CatalogSearchStoreFactory.swift +31 -0
- package/ios/CatalogService.swift +307 -0
- package/ios/ExpoAppleMusic.podspec +29 -0
- package/ios/ExpoAppleMusicModule.swift +505 -0
- package/ios/HistoryService.swift +53 -0
- package/ios/LibraryMutationsService.swift +63 -0
- package/ios/LibraryService.swift +313 -0
- package/ios/MusicItemMapper.swift +171 -0
- package/ios/MusicKitAuthStorage.swift +38 -0
- package/ios/MusicKitCatalogSearchStore.swift +62 -0
- package/ios/PlaybackController.swift +201 -0
- package/ios/PlaybackObserver.swift +225 -0
- package/ios/QueueService.swift +166 -0
- package/ios/RatingsService.swift +66 -0
- package/ios/RecommendationsService.swift +34 -0
- package/ios/RestCatalogSearchStore.swift +62 -0
- package/ios/RestJsonMapper.swift +268 -0
- package/ios/StorefrontService.swift +55 -0
- package/ios/SubscriptionService.swift +119 -0
- package/ios/bridge/ExpoBridgeCatalog.swift +98 -0
- package/ios/bridge/ExpoBridgeHistory.swift +71 -0
- package/ios/bridge/ExpoBridgeLibrary.swift +93 -0
- package/ios/bridge/ExpoBridgeRecommendations.swift +28 -0
- package/package.json +89 -0
- package/plugin/build/index.d.ts +5 -0
- package/plugin/build/index.js +10 -0
- package/plugin/build/with-expo-apple-music.d.ts +10 -0
- package/plugin/build/with-expo-apple-music.js +50 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// LibraryService.swift
|
|
2
|
+
// Handles user's Apple Music library operations.
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MusicKit
|
|
6
|
+
|
|
7
|
+
@available(iOS 16.0, *)
|
|
8
|
+
final class LibraryService {
|
|
9
|
+
|
|
10
|
+
// MARK: - Options
|
|
11
|
+
|
|
12
|
+
func getArtists(musicUserToken: String, options: BridgePagination) async throws -> [[String: Any]] {
|
|
13
|
+
let data = try await AppleMusicRestClient.getDataArray(
|
|
14
|
+
path: "/v1/me/library/artists",
|
|
15
|
+
musicUserToken: musicUserToken,
|
|
16
|
+
query: ["limit": "\(options.limit)", "offset": "\(options.offset)"]
|
|
17
|
+
)
|
|
18
|
+
return data.map(RestJsonMapper.mapArtist)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private func getArtistsNative(options: BridgePagination) async throws -> [[String: Any]] {
|
|
22
|
+
var request = MusicLibraryRequest<Artist>()
|
|
23
|
+
request.limit = options.limit
|
|
24
|
+
request.offset = options.offset
|
|
25
|
+
let response = try await request.response()
|
|
26
|
+
return response.items.map(MusicItemMapper.map)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func getAlbums(musicUserToken: String, options: BridgePagination) async throws -> [[String: Any]] {
|
|
30
|
+
let data = try await AppleMusicRestClient.getDataArray(
|
|
31
|
+
path: "/v1/me/library/albums",
|
|
32
|
+
musicUserToken: musicUserToken,
|
|
33
|
+
query: ["limit": "\(options.limit)", "offset": "\(options.offset)"]
|
|
34
|
+
)
|
|
35
|
+
return data.map(RestJsonMapper.mapAlbum)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private func getAlbumsNative(options: BridgePagination) async throws -> [[String: Any]] {
|
|
39
|
+
var request = MusicLibraryRequest<Album>()
|
|
40
|
+
request.limit = options.limit
|
|
41
|
+
request.offset = options.offset
|
|
42
|
+
let response = try await request.response()
|
|
43
|
+
return response.items.map(MusicItemMapper.map)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// MARK: - Playlists
|
|
47
|
+
|
|
48
|
+
func getPlaylists(musicUserToken: String, options: BridgePagination) async throws -> [[String: Any]] {
|
|
49
|
+
let data = try await AppleMusicRestClient.getDataArray(
|
|
50
|
+
path: "/v1/me/library/playlists",
|
|
51
|
+
musicUserToken: musicUserToken,
|
|
52
|
+
query: ["limit": "\(options.limit)", "offset": "\(options.offset)"]
|
|
53
|
+
)
|
|
54
|
+
return data.map(RestJsonMapper.mapPlaylist)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func getPlaylistsNative(options: BridgePagination) async throws -> [[String: Any]] {
|
|
58
|
+
var request = MusicLibraryRequest<Playlist>()
|
|
59
|
+
request.limit = options.limit
|
|
60
|
+
request.offset = options.offset
|
|
61
|
+
|
|
62
|
+
let response = try await request.response()
|
|
63
|
+
|
|
64
|
+
// Load tracks for accurate count
|
|
65
|
+
var playlists: [[String: Any]] = []
|
|
66
|
+
for playlist in response.items {
|
|
67
|
+
let detailed = try await playlist.with([.tracks])
|
|
68
|
+
playlists.append(MusicItemMapper.map(detailed))
|
|
69
|
+
}
|
|
70
|
+
return playlists
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func getPlaylistSongs(musicUserToken: String, playlistId: String) async throws -> [[String: Any]] {
|
|
74
|
+
let json = try await AppleMusicRestClient.get(
|
|
75
|
+
path: "/v1/me/library/playlists/\(playlistId)/tracks",
|
|
76
|
+
musicUserToken: musicUserToken
|
|
77
|
+
)
|
|
78
|
+
guard let data = json["data"] as? [[String: Any]] else {
|
|
79
|
+
throw LibraryServiceError.playlistNotFound
|
|
80
|
+
}
|
|
81
|
+
return data.compactMap { row -> [String: Any]? in
|
|
82
|
+
guard let type = row["type"] as? String, type.contains("song") else { return nil }
|
|
83
|
+
return RestJsonMapper.mapSong(row)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private func getPlaylistSongsNative(playlistId: String) async throws -> [[String: Any]] {
|
|
88
|
+
let musicItemId = MusicItemID(playlistId)
|
|
89
|
+
|
|
90
|
+
var request = MusicLibraryRequest<Playlist>()
|
|
91
|
+
request.filter(matching: \.id, equalTo: musicItemId)
|
|
92
|
+
|
|
93
|
+
let response = try await request.response()
|
|
94
|
+
guard let playlist = response.items.first else {
|
|
95
|
+
throw LibraryServiceError.playlistNotFound
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let detailed = try await playlist.with([.tracks])
|
|
99
|
+
var songs: [[String: Any]] = []
|
|
100
|
+
|
|
101
|
+
if let tracks = detailed.tracks {
|
|
102
|
+
for track in tracks {
|
|
103
|
+
if case .song(let song) = track {
|
|
104
|
+
songs.append(MusicItemMapper.map(song))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return songs
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// MARK: - Songs
|
|
112
|
+
|
|
113
|
+
func getSongs(musicUserToken: String, options: BridgePagination) async throws -> [[String: Any]] {
|
|
114
|
+
let data = try await AppleMusicRestClient.getDataArray(
|
|
115
|
+
path: "/v1/me/library/songs",
|
|
116
|
+
musicUserToken: musicUserToken,
|
|
117
|
+
query: ["limit": "\(options.limit)", "offset": "\(options.offset)"]
|
|
118
|
+
)
|
|
119
|
+
return data.map(RestJsonMapper.mapSong)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private func getSongsNative(options: BridgePagination) async throws -> [[String: Any]] {
|
|
123
|
+
var request = MusicLibraryRequest<Song>()
|
|
124
|
+
request.limit = options.limit
|
|
125
|
+
request.offset = options.offset
|
|
126
|
+
|
|
127
|
+
let response = try await request.response()
|
|
128
|
+
return response.items.map(MusicItemMapper.map)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func getMusicVideos(musicUserToken: String, options: BridgePagination) async throws -> [[String: Any]] {
|
|
132
|
+
let data = try await AppleMusicRestClient.getDataArray(
|
|
133
|
+
path: "/v1/me/library/music-videos",
|
|
134
|
+
musicUserToken: musicUserToken,
|
|
135
|
+
query: ["limit": "\(options.limit)", "offset": "\(options.offset)"]
|
|
136
|
+
)
|
|
137
|
+
return data.map(RestJsonMapper.mapMusicVideo)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private func getMusicVideosNative(options: BridgePagination) async throws -> [[String: Any]] {
|
|
141
|
+
var request = MusicLibraryRequest<MusicVideo>()
|
|
142
|
+
request.limit = options.limit
|
|
143
|
+
request.offset = options.offset
|
|
144
|
+
let response = try await request.response()
|
|
145
|
+
return response.items.map(MusicItemMapper.map)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
struct LibrarySearchResult {
|
|
149
|
+
let songs: [[String: Any]]
|
|
150
|
+
let albums: [[String: Any]]
|
|
151
|
+
let artists: [[String: Any]]
|
|
152
|
+
let playlists: [[String: Any]]
|
|
153
|
+
let musicVideos: [[String: Any]]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func search(musicUserToken: String, term: String, types: [String], options: BridgePagination) async throws -> LibrarySearchResult {
|
|
157
|
+
return try await searchViaRest(musicUserToken: musicUserToken, term: term, types: types, options: options)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private func searchNative(term: String, types: [String], options: BridgePagination) async throws -> LibrarySearchResult {
|
|
161
|
+
if options.offset > 0 {
|
|
162
|
+
return try await searchViaRest(musicUserToken: "", term: term, types: types, options: options)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let searchTypes = types.compactMap { Self.librarySearchableType($0) }
|
|
166
|
+
let resolvedTypes: [any MusicLibrarySearchable.Type]
|
|
167
|
+
if searchTypes.isEmpty {
|
|
168
|
+
resolvedTypes = [Song.self, Album.self, Artist.self, Playlist.self, MusicVideo.self]
|
|
169
|
+
} else {
|
|
170
|
+
resolvedTypes = searchTypes
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
var request = MusicLibrarySearchRequest(term: term, types: resolvedTypes)
|
|
174
|
+
request.limit = options.limit
|
|
175
|
+
let response = try await request.response()
|
|
176
|
+
|
|
177
|
+
return LibrarySearchResult(
|
|
178
|
+
songs: response.songs.map(MusicItemMapper.map),
|
|
179
|
+
albums: response.albums.map(MusicItemMapper.map),
|
|
180
|
+
artists: response.artists.map(MusicItemMapper.map),
|
|
181
|
+
playlists: response.playlists.map(MusicItemMapper.map),
|
|
182
|
+
musicVideos: response.musicVideos.map(MusicItemMapper.map)
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// MusicKit `MusicLibrarySearchRequest` has no `offset`; REST matches Android pagination.
|
|
187
|
+
private func searchViaRest(
|
|
188
|
+
musicUserToken: String,
|
|
189
|
+
term: String,
|
|
190
|
+
types: [String],
|
|
191
|
+
options: BridgePagination
|
|
192
|
+
) async throws -> LibrarySearchResult {
|
|
193
|
+
let typeParam = Array(Set(types.compactMap { Self.librarySearchRestTypeParam($0) })).sorted().joined(
|
|
194
|
+
separator: ",")
|
|
195
|
+
let typesQuery = typeParam.isEmpty ? "library-songs,library-albums" : typeParam
|
|
196
|
+
|
|
197
|
+
let json = try await AppleMusicRestClient.get(
|
|
198
|
+
path: "/v1/me/library/search",
|
|
199
|
+
musicUserToken: musicUserToken,
|
|
200
|
+
query: [
|
|
201
|
+
"term": term,
|
|
202
|
+
"types": typesQuery,
|
|
203
|
+
"limit": "\(options.limit)",
|
|
204
|
+
"offset": "\(options.offset)",
|
|
205
|
+
]
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
let results = json["results"] as? [String: Any] ?? [:]
|
|
209
|
+
return LibrarySearchResult(
|
|
210
|
+
songs: parseLibrarySearchBucket(results: results, key: "library-songs", mapper: RestJsonMapper.mapSong),
|
|
211
|
+
albums: parseLibrarySearchBucket(results: results, key: "library-albums", mapper: RestJsonMapper.mapAlbum),
|
|
212
|
+
artists: parseLibrarySearchBucket(results: results, key: "library-artists", mapper: RestJsonMapper.mapArtist),
|
|
213
|
+
playlists: parseLibrarySearchBucket(
|
|
214
|
+
results: results, key: "library-playlists", mapper: RestJsonMapper.mapPlaylist),
|
|
215
|
+
musicVideos: parseLibrarySearchBucket(
|
|
216
|
+
results: results, key: "library-music-videos", mapper: RestJsonMapper.mapMusicVideo)
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private func parseLibrarySearchBucket(
|
|
221
|
+
results: [String: Any],
|
|
222
|
+
key: String,
|
|
223
|
+
mapper: ([String: Any]) -> [String: Any]
|
|
224
|
+
) -> [[String: Any]] {
|
|
225
|
+
guard let bucket = results[key] as? [String: Any],
|
|
226
|
+
let data = bucket["data"] as? [[String: Any]]
|
|
227
|
+
else {
|
|
228
|
+
return []
|
|
229
|
+
}
|
|
230
|
+
return data.map(mapper)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private static func librarySearchRestTypeParam(_ type: String) -> String? {
|
|
234
|
+
switch type {
|
|
235
|
+
case "library-songs", "songs": return "library-songs"
|
|
236
|
+
case "library-albums", "albums": return "library-albums"
|
|
237
|
+
case "library-artists", "artists": return "library-artists"
|
|
238
|
+
case "library-playlists", "playlists": return "library-playlists"
|
|
239
|
+
case "library-music-videos", "music-videos", "musicVideos": return "library-music-videos"
|
|
240
|
+
default: return nil
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private static func librarySearchableType(_ type: String) -> (any MusicLibrarySearchable.Type)? {
|
|
245
|
+
switch type {
|
|
246
|
+
case "library-songs", "songs": return Song.self
|
|
247
|
+
case "library-albums", "albums": return Album.self
|
|
248
|
+
case "library-artists", "artists": return Artist.self
|
|
249
|
+
case "library-playlists", "playlists": return Playlist.self
|
|
250
|
+
case "library-music-videos", "music-videos", "musicVideos": return MusicVideo.self
|
|
251
|
+
default: return nil
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// MARK: - Fetch Items by ID
|
|
256
|
+
|
|
257
|
+
func fetchSong(id: MusicItemID) async throws -> Song? {
|
|
258
|
+
var request = MusicLibraryRequest<Song>()
|
|
259
|
+
request.filter(matching: \.id, equalTo: id)
|
|
260
|
+
let response = try await request.response()
|
|
261
|
+
return response.items.first
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
func fetchAlbum(id: MusicItemID) async throws -> Album? {
|
|
265
|
+
var request = MusicLibraryRequest<Album>()
|
|
266
|
+
request.filter(matching: \.id, equalTo: id)
|
|
267
|
+
let response = try await request.response()
|
|
268
|
+
return response.items.first
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
func fetchPlaylist(id: MusicItemID) async throws -> Playlist? {
|
|
272
|
+
var request = MusicLibraryRequest<Playlist>()
|
|
273
|
+
request.filter(matching: \.id, equalTo: id)
|
|
274
|
+
let response = try await request.response()
|
|
275
|
+
guard let playlist = response.items.first else { return nil }
|
|
276
|
+
return try await playlist.with([.tracks])
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// MARK: - Extract Songs from Playlist
|
|
280
|
+
|
|
281
|
+
func extractSongs(from playlist: Playlist) async throws -> [Song] {
|
|
282
|
+
let detailed = try await playlist.with([.tracks])
|
|
283
|
+
guard let tracks = detailed.tracks else { return [] }
|
|
284
|
+
|
|
285
|
+
var songs: [Song] = []
|
|
286
|
+
for track in tracks {
|
|
287
|
+
if case .song(let song) = track {
|
|
288
|
+
songs.append(song)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return songs
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// MARK: - Errors
|
|
296
|
+
|
|
297
|
+
enum LibraryServiceError: LocalizedError {
|
|
298
|
+
case playlistNotFound
|
|
299
|
+
case songNotFound
|
|
300
|
+
case albumNotFound
|
|
301
|
+
case noTracksInPlaylist
|
|
302
|
+
case noSongsInPlaylist
|
|
303
|
+
|
|
304
|
+
var errorDescription: String? {
|
|
305
|
+
switch self {
|
|
306
|
+
case .playlistNotFound: return "Playlist not found in library"
|
|
307
|
+
case .songNotFound: return "Song not found in library"
|
|
308
|
+
case .albumNotFound: return "Album not found in library"
|
|
309
|
+
case .noTracksInPlaylist: return "No tracks in playlist"
|
|
310
|
+
case .noSongsInPlaylist: return "No songs in playlist"
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// MusicItemMapper.swift
|
|
2
|
+
// Maps MusicKit types to bridge dictionaries (must match RestJsonMapper / AppleMusicJsonMapper).
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MusicKit
|
|
6
|
+
|
|
7
|
+
@available(iOS 16.0, *)
|
|
8
|
+
enum MusicItemMapper {
|
|
9
|
+
|
|
10
|
+
// MARK: - Song
|
|
11
|
+
|
|
12
|
+
static func map(_ song: Song) -> [String: Any] {
|
|
13
|
+
[
|
|
14
|
+
"id": catalogPlaybackId(from: song),
|
|
15
|
+
"title": song.title,
|
|
16
|
+
"artistName": song.artistName,
|
|
17
|
+
"artworkUrl": extractArtworkURL(song.artwork),
|
|
18
|
+
"duration": durationMillis(song.duration),
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// MARK: - Album
|
|
23
|
+
|
|
24
|
+
static func map(_ album: Album) -> [String: Any] {
|
|
25
|
+
[
|
|
26
|
+
"id": musicItemId(album.id),
|
|
27
|
+
"title": album.title,
|
|
28
|
+
"artistName": album.artistName,
|
|
29
|
+
"artworkUrl": extractArtworkURL(album.artwork),
|
|
30
|
+
"trackCount": album.trackCount,
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// MARK: - Artist
|
|
35
|
+
|
|
36
|
+
static func map(_ artist: Artist) -> [String: Any] {
|
|
37
|
+
[
|
|
38
|
+
"id": musicItemId(artist.id),
|
|
39
|
+
"name": artist.name,
|
|
40
|
+
"artworkUrl": extractArtworkURL(artist.artwork),
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MARK: - Playlist
|
|
45
|
+
|
|
46
|
+
static func map(_ playlist: Playlist) -> [String: Any] {
|
|
47
|
+
[
|
|
48
|
+
"id": musicItemId(playlist.id),
|
|
49
|
+
"name": playlist.name,
|
|
50
|
+
"description": playlist.standardDescription ?? "",
|
|
51
|
+
"artworkUrl": extractArtworkURL(playlist.artwork),
|
|
52
|
+
"trackCount": playlistTrackCount(playlist),
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// MARK: - Station
|
|
57
|
+
|
|
58
|
+
static func map(_ station: Station) -> [String: Any] {
|
|
59
|
+
[
|
|
60
|
+
"id": musicItemId(station.id),
|
|
61
|
+
"name": station.name,
|
|
62
|
+
"artworkUrl": extractArtworkURL(station.artwork),
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// MARK: - Music Video
|
|
67
|
+
|
|
68
|
+
@available(iOS 16.0, *)
|
|
69
|
+
static func map(_ musicVideo: MusicVideo) -> [String: Any] {
|
|
70
|
+
[
|
|
71
|
+
"id": catalogPlaybackId(from: musicVideo),
|
|
72
|
+
"title": musicVideo.title,
|
|
73
|
+
"artistName": musicVideo.artistName,
|
|
74
|
+
"artworkUrl": extractArtworkURL(musicVideo.artwork),
|
|
75
|
+
"duration": durationMillis(musicVideo.duration),
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - Recently Played Items
|
|
80
|
+
|
|
81
|
+
@available(iOS 16.0, *)
|
|
82
|
+
static func map(_ item: RecentlyPlayedMusicItem) -> [String: Any] {
|
|
83
|
+
var result: [String: Any] = [
|
|
84
|
+
"id": musicItemId(item.id),
|
|
85
|
+
"title": item.title,
|
|
86
|
+
"subtitle": String(describing: item.subtitle ?? ""),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
switch item {
|
|
90
|
+
case .album:
|
|
91
|
+
result["type"] = "album"
|
|
92
|
+
case .playlist:
|
|
93
|
+
result["type"] = "playlist"
|
|
94
|
+
case .station:
|
|
95
|
+
result["type"] = "station"
|
|
96
|
+
default:
|
|
97
|
+
result["type"] = "unknown"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// MARK: - Playback Status
|
|
104
|
+
|
|
105
|
+
static func describePlaybackStatus(_ status: MusicPlayer.PlaybackStatus) -> String {
|
|
106
|
+
switch status {
|
|
107
|
+
case .playing: return "playing"
|
|
108
|
+
case .paused: return "paused"
|
|
109
|
+
case .stopped: return "stopped"
|
|
110
|
+
case .interrupted: return "interrupted"
|
|
111
|
+
case .seekingForward: return "seekingForward"
|
|
112
|
+
case .seekingBackward: return "seekingBackward"
|
|
113
|
+
@unknown default: return "unknown"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// MARK: - ID & duration helpers (parity with RestJsonMapper / Android)
|
|
118
|
+
|
|
119
|
+
static func musicItemId(_ id: MusicItemID) -> String {
|
|
120
|
+
id.rawValue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static func catalogPlaybackId(from song: Song) -> String {
|
|
124
|
+
if let playId = catalogPlaybackId(from: song.playParameters) {
|
|
125
|
+
return playId
|
|
126
|
+
}
|
|
127
|
+
return musicItemId(song.id)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static func catalogPlaybackId(from musicVideo: MusicVideo) -> String {
|
|
131
|
+
if let playId = catalogPlaybackId(from: musicVideo.playParameters) {
|
|
132
|
+
return playId
|
|
133
|
+
}
|
|
134
|
+
return musicItemId(musicVideo.id)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// MusicKit no longer exposes `PlayParameters.id`; decode Codable payload (parity with RestJsonMapper).
|
|
138
|
+
private static func catalogPlaybackId(from playParameters: PlayParameters?) -> String? {
|
|
139
|
+
guard let playParameters,
|
|
140
|
+
let data = try? JSONEncoder().encode(playParameters),
|
|
141
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
142
|
+
else { return nil }
|
|
143
|
+
if let id = json["id"] as? String, !id.isEmpty { return id }
|
|
144
|
+
if let catalogId = json["catalogId"] as? String, !catalogId.isEmpty { return catalogId }
|
|
145
|
+
return nil
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// MusicKit `duration` is seconds; bridge uses milliseconds (matches `durationInMillis` from REST).
|
|
149
|
+
static func durationMillis(_ seconds: TimeInterval?) -> Int {
|
|
150
|
+
Int((seconds ?? 0) * 1000)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private static func playlistTrackCount(_ playlist: Playlist) -> Int {
|
|
154
|
+
if let count = playlist.tracks?.count, count > 0 {
|
|
155
|
+
return count
|
|
156
|
+
}
|
|
157
|
+
return playlist.entries?.count ?? 0
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// MARK: - Private Helpers
|
|
161
|
+
|
|
162
|
+
private static func extractArtworkURL(_ artwork: Artwork?, width: Int = 200, height: Int = 200) -> String {
|
|
163
|
+
guard let artwork = artwork,
|
|
164
|
+
let url = artwork.url(width: width, height: height),
|
|
165
|
+
url.scheme == "https" || url.scheme == "http"
|
|
166
|
+
else {
|
|
167
|
+
return ""
|
|
168
|
+
}
|
|
169
|
+
return url.absoluteString
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// MusicKitAuthStorage.swift
|
|
2
|
+
// Persists developer and music user tokens for Apple Music REST (parity with Android).
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
|
|
6
|
+
@available(iOS 16.0, *)
|
|
7
|
+
enum MusicKitAuthStorage {
|
|
8
|
+
private static let developerTokenKey = "expo.modules.applemusic.developerToken"
|
|
9
|
+
private static let musicUserTokenKey = "expo.modules.applemusic.musicUserToken"
|
|
10
|
+
|
|
11
|
+
static func saveDeveloperToken(_ token: String) {
|
|
12
|
+
UserDefaults.standard.set(token, forKey: developerTokenKey)
|
|
13
|
+
AuthenticatedSessionCache.invalidate()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static func getDeveloperToken() -> String? {
|
|
17
|
+
UserDefaults.standard.string(forKey: developerTokenKey)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static func saveMusicUserToken(_ token: String) {
|
|
21
|
+
UserDefaults.standard.set(token, forKey: musicUserTokenKey)
|
|
22
|
+
AuthenticatedSessionCache.invalidate()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static func getMusicUserToken() -> String? {
|
|
26
|
+
UserDefaults.standard.string(forKey: musicUserTokenKey)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static func clearMusicUserToken() {
|
|
30
|
+
UserDefaults.standard.removeObject(forKey: musicUserTokenKey)
|
|
31
|
+
AuthenticatedSessionCache.invalidate()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static func clearDeveloperToken() {
|
|
35
|
+
UserDefaults.standard.removeObject(forKey: developerTokenKey)
|
|
36
|
+
AuthenticatedSessionCache.invalidate()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// MusicKitCatalogSearchStore.swift
|
|
2
|
+
// Catalog search via native MusicCatalogSearchRequest.
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MusicKit
|
|
6
|
+
|
|
7
|
+
@available(iOS 16.0, *)
|
|
8
|
+
struct MusicKitCatalogSearchStore: CatalogSearchStore {
|
|
9
|
+
|
|
10
|
+
static let missingDeveloperTokenMessage =
|
|
11
|
+
"Apple Music catalog search needs a developer JWT on this device. "
|
|
12
|
+
+ "From the repo root: npm run dev-token -- --write-env example/.env.local "
|
|
13
|
+
+ "then restart Metro (npx expo start --clear), rebuild, and tap Authorize. "
|
|
14
|
+
+ "See docs/CLI.md. (Native MusicKit auto-token returned 404 for this bundle ID.)"
|
|
15
|
+
|
|
16
|
+
func search(
|
|
17
|
+
term: String,
|
|
18
|
+
types: [String],
|
|
19
|
+
options: CatalogService.SearchOptions
|
|
20
|
+
) async throws -> CatalogService.SearchResult {
|
|
21
|
+
let searchTypes = types.compactMap { typeString -> MusicCatalogSearchable.Type? in
|
|
22
|
+
switch typeString {
|
|
23
|
+
case "songs": return Song.self
|
|
24
|
+
case "albums": return Album.self
|
|
25
|
+
case "artists": return Artist.self
|
|
26
|
+
case "playlists": return Playlist.self
|
|
27
|
+
case "stations": return Station.self
|
|
28
|
+
case "music-videos": return MusicVideo.self
|
|
29
|
+
default: return nil
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var request = MusicCatalogSearchRequest(term: term, types: searchTypes)
|
|
34
|
+
request.limit = options.limit
|
|
35
|
+
request.offset = options.offset
|
|
36
|
+
|
|
37
|
+
let response = try await request.response()
|
|
38
|
+
|
|
39
|
+
return CatalogService.SearchResult(
|
|
40
|
+
songs: response.songs.map(MusicItemMapper.map),
|
|
41
|
+
albums: response.albums.map(MusicItemMapper.map),
|
|
42
|
+
artists: response.artists.map(MusicItemMapper.map),
|
|
43
|
+
playlists: response.playlists.map(MusicItemMapper.map),
|
|
44
|
+
stations: response.stations.map(MusicItemMapper.map),
|
|
45
|
+
musicVideos: response.musicVideos.map(MusicItemMapper.map)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static func isClientNotRegistered(_ error: Error) -> Bool {
|
|
50
|
+
let ns = error as NSError
|
|
51
|
+
if ns.domain == "ICError", ns.code == -8200 { return true }
|
|
52
|
+
if ns.localizedDescription.contains("Client not found") { return true }
|
|
53
|
+
if ns.localizedDescription.contains("developerTokenRequestFailed") { return true }
|
|
54
|
+
if let underlying = ns.userInfo[NSUnderlyingErrorKey] as? NSError,
|
|
55
|
+
underlying.domain == "AMSErrorDomain",
|
|
56
|
+
underlying.code == 301
|
|
57
|
+
{
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
}
|