@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,62 @@
|
|
|
1
|
+
// RestCatalogSearchStore.swift
|
|
2
|
+
// Catalog search via Apple Music REST API (aligned with Android catalog paths).
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
|
|
6
|
+
@available(iOS 16.0, *)
|
|
7
|
+
struct RestCatalogSearchStore: CatalogSearchStore {
|
|
8
|
+
|
|
9
|
+
func search(
|
|
10
|
+
term: String,
|
|
11
|
+
types: [String],
|
|
12
|
+
options: CatalogService.SearchOptions
|
|
13
|
+
) async throws -> CatalogService.SearchResult {
|
|
14
|
+
let storefront = StorefrontService.getCatalogStorefront()
|
|
15
|
+
let typeParam = Array(Set(types.compactMap { catalogSearchTypeParam($0) })).sorted().joined(
|
|
16
|
+
separator: ",")
|
|
17
|
+
let typesQuery = typeParam.isEmpty ? "songs,albums" : typeParam
|
|
18
|
+
|
|
19
|
+
let json = try await AppleMusicRestClient.get(
|
|
20
|
+
path: "/v1/catalog/\(storefront)/search",
|
|
21
|
+
query: [
|
|
22
|
+
"term": term,
|
|
23
|
+
"types": typesQuery,
|
|
24
|
+
"limit": "\(options.limit)",
|
|
25
|
+
"offset": "\(options.offset)",
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
let results = json["results"] as? [String: Any] ?? [:]
|
|
30
|
+
|
|
31
|
+
return CatalogService.SearchResult(
|
|
32
|
+
songs: parseSearchBucket(results: results, key: "songs", mapper: RestJsonMapper.mapSong),
|
|
33
|
+
albums: parseSearchBucket(results: results, key: "albums", mapper: RestJsonMapper.mapAlbum),
|
|
34
|
+
artists: parseSearchBucket(results: results, key: "artists", mapper: RestJsonMapper.mapArtist),
|
|
35
|
+
playlists: parseSearchBucket(
|
|
36
|
+
results: results, key: "playlists", mapper: RestJsonMapper.mapPlaylist),
|
|
37
|
+
stations: parseSearchBucket(results: results, key: "stations", mapper: RestJsonMapper.mapStation),
|
|
38
|
+
musicVideos: parseSearchBucket(
|
|
39
|
+
results: results, key: "music-videos", mapper: RestJsonMapper.mapMusicVideo)
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private func catalogSearchTypeParam(_ type: String) -> String? {
|
|
44
|
+
switch type {
|
|
45
|
+
case "songs", "albums", "artists", "playlists", "stations", "music-videos": return type
|
|
46
|
+
default: return nil
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private func parseSearchBucket(
|
|
51
|
+
results: [String: Any],
|
|
52
|
+
key: String,
|
|
53
|
+
mapper: ([String: Any]) -> [String: Any]
|
|
54
|
+
) -> [[String: Any]] {
|
|
55
|
+
guard let bucket = results[key] as? [String: Any],
|
|
56
|
+
let data = bucket["data"] as? [[String: Any]]
|
|
57
|
+
else {
|
|
58
|
+
return []
|
|
59
|
+
}
|
|
60
|
+
return data.map(mapper)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// RestJsonMapper.swift
|
|
2
|
+
// Maps Apple Music API JSON objects to bridge dictionaries (matches Android mapper).
|
|
3
|
+
// Golden REST cases: fixtures/*.json + fixtures/expected/*.json — see docs/BRIDGE_CONTRACT.md.
|
|
4
|
+
|
|
5
|
+
import Foundation
|
|
6
|
+
|
|
7
|
+
@available(iOS 16.0, *)
|
|
8
|
+
enum RestJsonMapper {
|
|
9
|
+
|
|
10
|
+
static func mapAlbum(_ resource: [String: Any]) -> [String: Any] {
|
|
11
|
+
let attributes = resource["attributes"] as? [String: Any] ?? [:]
|
|
12
|
+
let trackCount: Int
|
|
13
|
+
if let count = attributes["trackCount"] as? Int {
|
|
14
|
+
trackCount = count
|
|
15
|
+
} else if let count = attributes["trackCount"] as? Double {
|
|
16
|
+
trackCount = Int(count)
|
|
17
|
+
} else {
|
|
18
|
+
trackCount = 0
|
|
19
|
+
}
|
|
20
|
+
return [
|
|
21
|
+
"id": resource["id"] as? String ?? "",
|
|
22
|
+
"title": attributes["name"] as? String ?? "",
|
|
23
|
+
"artistName": attributes["artistName"] as? String ?? "",
|
|
24
|
+
"artworkUrl": artworkUrl(attributes["artwork"] as? [String: Any]),
|
|
25
|
+
"trackCount": trackCount,
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static func mapSong(_ resource: [String: Any]) -> [String: Any] {
|
|
30
|
+
let attributes = resource["attributes"] as? [String: Any] ?? [:]
|
|
31
|
+
let id = catalogPlaybackId(resource) ?? (resource["id"] as? String ?? "")
|
|
32
|
+
return [
|
|
33
|
+
"id": id,
|
|
34
|
+
"title": attributes["name"] as? String ?? "",
|
|
35
|
+
"artistName": attributes["artistName"] as? String ?? "",
|
|
36
|
+
"artworkUrl": artworkUrl(attributes["artwork"] as? [String: Any]),
|
|
37
|
+
"duration": durationMillis(attributes),
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static func mapArtist(_ resource: [String: Any]) -> [String: Any] {
|
|
42
|
+
let attributes = resource["attributes"] as? [String: Any] ?? [:]
|
|
43
|
+
return [
|
|
44
|
+
"id": resource["id"] as? String ?? "",
|
|
45
|
+
"name": attributes["name"] as? String ?? "",
|
|
46
|
+
"artworkUrl": artworkUrl(attributes["artwork"] as? [String: Any]),
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static func mapRecentResource(_ resource: [String: Any]) -> [String: Any] {
|
|
51
|
+
let attributes = resource["attributes"] as? [String: Any] ?? [:]
|
|
52
|
+
let apiType = resource["type"] as? String ?? ""
|
|
53
|
+
let itemType: String
|
|
54
|
+
if apiType.contains("album") {
|
|
55
|
+
itemType = "album"
|
|
56
|
+
} else if apiType.contains("playlist") {
|
|
57
|
+
itemType = "playlist"
|
|
58
|
+
} else if apiType.contains("station") {
|
|
59
|
+
itemType = "station"
|
|
60
|
+
} else {
|
|
61
|
+
itemType = "unknown"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var subtitle = attributes["artistName"] as? String ?? ""
|
|
65
|
+
if subtitle.isEmpty {
|
|
66
|
+
subtitle = attributes["curatorName"] as? String ?? ""
|
|
67
|
+
}
|
|
68
|
+
if subtitle.isEmpty, let description = attributes["description"] as? [String: Any] {
|
|
69
|
+
subtitle = description["standard"] as? String ?? ""
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
"id": resource["id"] as? String ?? "",
|
|
74
|
+
"title": attributes["name"] as? String ?? "",
|
|
75
|
+
"subtitle": subtitle,
|
|
76
|
+
"type": itemType,
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static func mapRecentlyPlayed(_ resource: [String: Any]) -> [String: Any] {
|
|
81
|
+
mapRecentResource(resource)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static func mapPlaylist(_ resource: [String: Any]) -> [String: Any] {
|
|
85
|
+
let attributes = resource["attributes"] as? [String: Any] ?? [:]
|
|
86
|
+
var trackCount = 0
|
|
87
|
+
if let count = attributes["trackCount"] as? Int {
|
|
88
|
+
trackCount = count
|
|
89
|
+
} else if let count = attributes["trackCount"] as? Double {
|
|
90
|
+
trackCount = Int(count)
|
|
91
|
+
}
|
|
92
|
+
var description = attributes["description"] as? String ?? ""
|
|
93
|
+
if description.isEmpty, let desc = attributes["description"] as? [String: Any] {
|
|
94
|
+
description = desc["standard"] as? String ?? ""
|
|
95
|
+
}
|
|
96
|
+
return [
|
|
97
|
+
"id": resource["id"] as? String ?? "",
|
|
98
|
+
"name": attributes["name"] as? String ?? "",
|
|
99
|
+
"description": description,
|
|
100
|
+
"artworkUrl": artworkUrl(attributes["artwork"] as? [String: Any]),
|
|
101
|
+
"trackCount": trackCount,
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static func mapMusicVideo(_ resource: [String: Any]) -> [String: Any] {
|
|
106
|
+
let attributes = resource["attributes"] as? [String: Any] ?? [:]
|
|
107
|
+
let id = catalogPlaybackId(resource) ?? (resource["id"] as? String ?? "")
|
|
108
|
+
return [
|
|
109
|
+
"id": id,
|
|
110
|
+
"title": attributes["name"] as? String ?? "",
|
|
111
|
+
"artistName": attributes["artistName"] as? String ?? "",
|
|
112
|
+
"artworkUrl": artworkUrl(attributes["artwork"] as? [String: Any]),
|
|
113
|
+
"duration": durationMillis(attributes),
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static func mapStation(_ resource: [String: Any]) -> [String: Any] {
|
|
118
|
+
let attributes = resource["attributes"] as? [String: Any] ?? [:]
|
|
119
|
+
return [
|
|
120
|
+
"id": resource["id"] as? String ?? "",
|
|
121
|
+
"name": attributes["name"] as? String ?? "",
|
|
122
|
+
"artworkUrl": artworkUrl(attributes["artwork"] as? [String: Any]),
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private static func catalogPlaybackId(_ resource: [String: Any]) -> String? {
|
|
127
|
+
let attributes = resource["attributes"] as? [String: Any] ?? [:]
|
|
128
|
+
guard let playParams = attributes["playParams"] as? [String: Any] else { return nil }
|
|
129
|
+
if let id = playParams["id"] as? String, !id.isEmpty { return id }
|
|
130
|
+
if let catalogId = playParams["catalogId"] as? String, !catalogId.isEmpty { return catalogId }
|
|
131
|
+
return nil
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Candidate catalog song ids for MusicKit lookup (playback id, resource id, playParams).
|
|
135
|
+
static func catalogSongLookupIds(primaryId: String, resource: [String: Any]) -> [String] {
|
|
136
|
+
var ids: [String] = []
|
|
137
|
+
func append(_ value: String?) {
|
|
138
|
+
guard let value, !value.isEmpty, !ids.contains(value) else { return }
|
|
139
|
+
ids.append(value)
|
|
140
|
+
}
|
|
141
|
+
append(primaryId)
|
|
142
|
+
append(resource["id"] as? String)
|
|
143
|
+
append(catalogPlaybackId(resource))
|
|
144
|
+
return ids
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static func durationMillis(_ attributes: [String: Any]) -> Int {
|
|
148
|
+
if let millis = attributes["durationInMillis"] as? Int {
|
|
149
|
+
return millis
|
|
150
|
+
}
|
|
151
|
+
if let millis = attributes["durationInMillis"] as? Double {
|
|
152
|
+
return Int(millis)
|
|
153
|
+
}
|
|
154
|
+
if let duration = attributes["duration"] as? Double {
|
|
155
|
+
return Int(duration * 1000)
|
|
156
|
+
}
|
|
157
|
+
return 0
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private static func artworkUrl(_ artwork: [String: Any]?, width: Int = 200, height: Int = 200) -> String {
|
|
161
|
+
guard let artwork, let template = artwork["url"] as? String, !template.isEmpty else {
|
|
162
|
+
return ""
|
|
163
|
+
}
|
|
164
|
+
return template
|
|
165
|
+
.replacingOccurrences(of: "{w}", with: "\(width)")
|
|
166
|
+
.replacingOccurrences(of: "{h}", with: "\(height)")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
static func mapRating(_ json: [String: Any]) -> [String: Any]? {
|
|
170
|
+
guard let data = json["data"] as? [[String: Any]], let first = data.first else {
|
|
171
|
+
return nil
|
|
172
|
+
}
|
|
173
|
+
let attributes = first["attributes"] as? [String: Any] ?? [:]
|
|
174
|
+
let value = attributes["value"] as? Int ?? (attributes["value"] as? Double).map { Int($0) }
|
|
175
|
+
guard let value else { return nil }
|
|
176
|
+
return [
|
|
177
|
+
"id": first["id"] as? String ?? "",
|
|
178
|
+
"value": value,
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
static func mapRecommendation(_ resource: [String: Any]) -> [String: Any] {
|
|
183
|
+
let attributes = resource["attributes"] as? [String: Any] ?? [:]
|
|
184
|
+
let titleDict = attributes["title"] as? [String: Any]
|
|
185
|
+
let title = titleDict?["stringForDisplay"] as? String ?? ""
|
|
186
|
+
let resourceTypes = attributes["resourceTypes"] as? [String] ?? []
|
|
187
|
+
let contents = mapRecommendationContents(resource)
|
|
188
|
+
return [
|
|
189
|
+
"id": resource["id"] as? String ?? "",
|
|
190
|
+
"title": title,
|
|
191
|
+
"resourceTypes": resourceTypes,
|
|
192
|
+
"playlists": contents.playlists,
|
|
193
|
+
"albums": contents.albums,
|
|
194
|
+
"stations": contents.stations,
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static func mapReplaySummary(_ resource: [String: Any]) -> [String: Any] {
|
|
199
|
+
let attributes = resource["attributes"] as? [String: Any] ?? [:]
|
|
200
|
+
let year = attributes["year"] as? Int ?? (attributes["year"] as? Double).map { Int($0) }
|
|
201
|
+
var result: [String: Any] = [
|
|
202
|
+
"id": resource["id"] as? String ?? "",
|
|
203
|
+
"type": resource["type"] as? String ?? "",
|
|
204
|
+
"name": attributes["name"] as? String ?? "",
|
|
205
|
+
"topSongs": mapRelationshipResources(resource, key: "top-songs", mapper: mapSong),
|
|
206
|
+
"topAlbums": mapRelationshipResources(resource, key: "top-albums", mapper: mapAlbum),
|
|
207
|
+
"topArtists": mapRelationshipResources(resource, key: "top-artists", mapper: mapArtist),
|
|
208
|
+
]
|
|
209
|
+
if let year {
|
|
210
|
+
result["year"] = year
|
|
211
|
+
}
|
|
212
|
+
return result
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private struct RecommendationContents {
|
|
216
|
+
let playlists: [[String: Any]]
|
|
217
|
+
let albums: [[String: Any]]
|
|
218
|
+
let stations: [[String: Any]]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static func mapRecommendationContents(_ resource: [String: Any]) -> RecommendationContents {
|
|
222
|
+
var playlists: [[String: Any]] = []
|
|
223
|
+
var albums: [[String: Any]] = []
|
|
224
|
+
var stations: [[String: Any]] = []
|
|
225
|
+
guard let relationships = resource["relationships"] as? [String: Any],
|
|
226
|
+
let contents = relationships["contents"] as? [String: Any],
|
|
227
|
+
let data = contents["data"] as? [[String: Any]]
|
|
228
|
+
else {
|
|
229
|
+
return RecommendationContents(playlists: playlists, albums: albums, stations: stations)
|
|
230
|
+
}
|
|
231
|
+
for item in data {
|
|
232
|
+
let type = item["type"] as? String ?? ""
|
|
233
|
+
if type.contains("playlist") {
|
|
234
|
+
playlists.append(mapPlaylist(item))
|
|
235
|
+
} else if type.contains("album") {
|
|
236
|
+
albums.append(mapAlbum(item))
|
|
237
|
+
} else if type.contains("station") {
|
|
238
|
+
stations.append(mapStation(item))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return RecommendationContents(playlists: playlists, albums: albums, stations: stations)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private static func mapRelationshipResources(
|
|
245
|
+
_ resource: [String: Any],
|
|
246
|
+
key: String,
|
|
247
|
+
mapper: ([String: Any]) -> [String: Any]
|
|
248
|
+
) -> [[String: Any]] {
|
|
249
|
+
guard let relationships = resource["relationships"] as? [String: Any],
|
|
250
|
+
let relation = relationships[key] as? [String: Any],
|
|
251
|
+
let data = relation["data"] as? [[String: Any]]
|
|
252
|
+
else {
|
|
253
|
+
return []
|
|
254
|
+
}
|
|
255
|
+
return data.map(mapper)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
static func buildIdsQuery(_ resourceIds: [String: [String]]) -> [String: String] {
|
|
259
|
+
var query: [String: String] = [:]
|
|
260
|
+
for (type, ids) in resourceIds {
|
|
261
|
+
let filtered = ids.filter { !$0.isEmpty }
|
|
262
|
+
if !filtered.isEmpty {
|
|
263
|
+
query["ids[\(type)]"] = filtered.joined(separator: ",")
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return query
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// StorefrontService.swift
|
|
2
|
+
// Resolves the user's Apple Music storefront via MusicKit-authenticated API requests.
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MusicKit
|
|
6
|
+
|
|
7
|
+
@available(iOS 16.0, *)
|
|
8
|
+
enum StorefrontService {
|
|
9
|
+
|
|
10
|
+
enum StorefrontError: LocalizedError {
|
|
11
|
+
case invalidResponse
|
|
12
|
+
case unauthorized
|
|
13
|
+
|
|
14
|
+
var errorDescription: String? {
|
|
15
|
+
switch self {
|
|
16
|
+
case .invalidResponse: return "Invalid storefront response from Apple Music"
|
|
17
|
+
case .unauthorized: return "Not authorized for Apple Music"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Catalog REST paths — cached storefront or device locale (no music user token).
|
|
23
|
+
static func getCatalogStorefront() -> String {
|
|
24
|
+
if let cached = AuthenticatedSessionCache.cachedStorefrontId() {
|
|
25
|
+
return cached
|
|
26
|
+
}
|
|
27
|
+
return localeStorefrontId()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static func getStorefrontId(musicUserToken: String) async throws -> String {
|
|
31
|
+
if let cached = AuthenticatedSessionCache.cachedStorefrontId() {
|
|
32
|
+
return cached
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let session = AuthenticatedSession.current
|
|
36
|
+
if session.hasDeveloperToken {
|
|
37
|
+
let data = try await AppleMusicRestClient.getDataArray(
|
|
38
|
+
path: "/v1/me/storefront",
|
|
39
|
+
musicUserToken: musicUserToken)
|
|
40
|
+
if let id = data.first?["id"] as? String, !id.isEmpty {
|
|
41
|
+
AuthenticatedSessionCache.setStorefrontId(id)
|
|
42
|
+
return id
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return localeStorefrontId()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Fallback when MusicKit auto-token or `/v1/me/storefront` is unavailable (catalog REST only).
|
|
49
|
+
static func localeStorefrontId() -> String {
|
|
50
|
+
if let region = Locale.current.region?.identifier, !region.isEmpty {
|
|
51
|
+
return region.lowercased()
|
|
52
|
+
}
|
|
53
|
+
return "us"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// SubscriptionService.swift
|
|
2
|
+
// Handles Apple Music authorization and subscription status checks.
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MusicKit
|
|
6
|
+
import StoreKit
|
|
7
|
+
|
|
8
|
+
@available(iOS 16.0, *)
|
|
9
|
+
final class SubscriptionService {
|
|
10
|
+
|
|
11
|
+
// MARK: - Authorization
|
|
12
|
+
|
|
13
|
+
enum AuthorizationStatus: String {
|
|
14
|
+
case authorized
|
|
15
|
+
case denied
|
|
16
|
+
case notDetermined
|
|
17
|
+
case restricted
|
|
18
|
+
case unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func requestAuthorization() async -> AuthorizationStatus {
|
|
22
|
+
await withCheckedContinuation { continuation in
|
|
23
|
+
SKCloudServiceController.requestAuthorization { status in
|
|
24
|
+
let result: AuthorizationStatus
|
|
25
|
+
switch status {
|
|
26
|
+
case .authorized: result = .authorized
|
|
27
|
+
case .denied: result = .denied
|
|
28
|
+
case .notDetermined: result = .notDetermined
|
|
29
|
+
case .restricted: result = .restricted
|
|
30
|
+
@unknown default: result = .unknown
|
|
31
|
+
}
|
|
32
|
+
continuation.resume(returning: result)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Fetches a music user token when a developer JWT is available (not persisted by the module).
|
|
38
|
+
func fetchMusicUserToken(developerToken: String) async -> String? {
|
|
39
|
+
guard !developerToken.isEmpty else { return nil }
|
|
40
|
+
return await withCheckedContinuation { continuation in
|
|
41
|
+
SKCloudServiceController().requestUserToken(forDeveloperToken: developerToken) { token, _ in
|
|
42
|
+
if let token, !token.isEmpty {
|
|
43
|
+
continuation.resume(returning: token)
|
|
44
|
+
} else {
|
|
45
|
+
continuation.resume(returning: nil)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// MARK: - Subscription Check
|
|
52
|
+
|
|
53
|
+
struct SubscriptionDetails {
|
|
54
|
+
let canPlayCatalogContent: Bool
|
|
55
|
+
let canBecomeSubscriber: Bool
|
|
56
|
+
let hasCloudLibraryEnabled: Bool
|
|
57
|
+
let isMusicCatalogSubscriptionEligible: Bool
|
|
58
|
+
|
|
59
|
+
func toDictionary() -> [String: Any] {
|
|
60
|
+
[
|
|
61
|
+
"canPlayCatalogContent": canPlayCatalogContent,
|
|
62
|
+
"canBecomeSubscriber": canBecomeSubscriber,
|
|
63
|
+
"hasCloudLibraryEnabled": hasCloudLibraryEnabled,
|
|
64
|
+
"isMusicCatalogSubscriptionEligible": isMusicCatalogSubscriptionEligible,
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func checkSubscription() async throws -> SubscriptionDetails {
|
|
70
|
+
let subscription = try await MusicSubscription.current
|
|
71
|
+
return SubscriptionDetails(
|
|
72
|
+
canPlayCatalogContent: subscription.canPlayCatalogContent,
|
|
73
|
+
canBecomeSubscriber: subscription.canBecomeSubscriber,
|
|
74
|
+
hasCloudLibraryEnabled: subscription.hasCloudLibraryEnabled,
|
|
75
|
+
isMusicCatalogSubscriptionEligible: subscription.canBecomeSubscriber
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// MARK: - Subscription Error Handling
|
|
81
|
+
|
|
82
|
+
@available(iOS 16.0, *)
|
|
83
|
+
extension SubscriptionService {
|
|
84
|
+
|
|
85
|
+
struct SubscriptionError: LocalizedError {
|
|
86
|
+
let code: String
|
|
87
|
+
let message: String
|
|
88
|
+
let failureReason: String?
|
|
89
|
+
let recoverySuggestion: String?
|
|
90
|
+
let helpAnchor: String?
|
|
91
|
+
|
|
92
|
+
var errorDescription: String? { message }
|
|
93
|
+
|
|
94
|
+
func toNSError() -> NSError {
|
|
95
|
+
var userInfo: [String: Any] = [NSLocalizedDescriptionKey: message]
|
|
96
|
+
if let reason = failureReason {
|
|
97
|
+
userInfo[NSLocalizedFailureReasonErrorKey] = reason
|
|
98
|
+
}
|
|
99
|
+
if let suggestion = recoverySuggestion {
|
|
100
|
+
userInfo[NSLocalizedRecoverySuggestionErrorKey] = suggestion
|
|
101
|
+
}
|
|
102
|
+
if let anchor = helpAnchor {
|
|
103
|
+
userInfo[NSHelpAnchorErrorKey] = anchor
|
|
104
|
+
}
|
|
105
|
+
return NSError(domain: "MusicSubscription", code: 0, userInfo: userInfo)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static func wrapSubscriptionError(_ error: Error) -> SubscriptionError? {
|
|
110
|
+
guard let subError = error as? MusicSubscription.Error else { return nil }
|
|
111
|
+
return SubscriptionError(
|
|
112
|
+
code: subError.rawValue,
|
|
113
|
+
message: subError.localizedDescription,
|
|
114
|
+
failureReason: subError.failureReason,
|
|
115
|
+
recoverySuggestion: subError.recoverySuggestion,
|
|
116
|
+
helpAnchor: subError.helpAnchor
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
@available(iOS 16.0, *)
|
|
4
|
+
enum ExpoBridgeCatalog {
|
|
5
|
+
static func catalogSearch(
|
|
6
|
+
service: CatalogService,
|
|
7
|
+
term: String,
|
|
8
|
+
types: [String],
|
|
9
|
+
options: NSDictionary
|
|
10
|
+
) async throws -> [String: Any] {
|
|
11
|
+
try await AppleMusicBridgeError.rethrow {
|
|
12
|
+
let pagination = BridgePagination(from: options)
|
|
13
|
+
let searchOptions = CatalogService.SearchOptions(limit: pagination.limit, offset: pagination.offset)
|
|
14
|
+
let result = try await service.search(term: term, types: types, options: searchOptions)
|
|
15
|
+
return BridgeResponses.catalogSearch(result)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static func getCatalogAlbumTracks(
|
|
20
|
+
service: CatalogService,
|
|
21
|
+
albumId: String,
|
|
22
|
+
options: NSDictionary
|
|
23
|
+
) async throws -> [String: Any] {
|
|
24
|
+
try await AppleMusicBridgeError.rethrow {
|
|
25
|
+
let pagination = BridgePagination(from: options)
|
|
26
|
+
let searchOptions = CatalogService.SearchOptions(limit: pagination.limit, offset: pagination.offset)
|
|
27
|
+
let songs = try await service.getAlbumTracks(albumId: albumId, options: searchOptions)
|
|
28
|
+
return BridgeResponses.songs(songs)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static func getCatalogArtistAlbums(
|
|
33
|
+
service: CatalogService,
|
|
34
|
+
artistId: String,
|
|
35
|
+
options: NSDictionary
|
|
36
|
+
) async throws -> [String: Any] {
|
|
37
|
+
try await AppleMusicBridgeError.rethrow {
|
|
38
|
+
let pagination = BridgePagination(from: options)
|
|
39
|
+
let searchOptions = CatalogService.SearchOptions(limit: pagination.limit, offset: pagination.offset)
|
|
40
|
+
let albums = try await service.getArtistAlbums(artistId: artistId, options: searchOptions)
|
|
41
|
+
return BridgeResponses.albums(albums)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static func getCatalogPlaylistTracks(
|
|
46
|
+
service: CatalogService,
|
|
47
|
+
playlistId: String,
|
|
48
|
+
options: NSDictionary
|
|
49
|
+
) async throws -> [String: Any] {
|
|
50
|
+
try await AppleMusicBridgeError.rethrow {
|
|
51
|
+
let pagination = BridgePagination(from: options)
|
|
52
|
+
let searchOptions = CatalogService.SearchOptions(limit: pagination.limit, offset: pagination.offset)
|
|
53
|
+
let songs = try await service.getPlaylistTracks(playlistId: playlistId, options: searchOptions)
|
|
54
|
+
return BridgeResponses.songs(songs)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static func getCatalogCharts(
|
|
59
|
+
service: CatalogService,
|
|
60
|
+
types: [String],
|
|
61
|
+
options: [String: Any]
|
|
62
|
+
) async throws -> [String: Any] {
|
|
63
|
+
try await AppleMusicBridgeError.rethrow {
|
|
64
|
+
let pagination = BridgePagination(from: options as NSDictionary)
|
|
65
|
+
let searchOptions = CatalogService.SearchOptions(limit: pagination.limit, offset: pagination.offset)
|
|
66
|
+
let genre = options["genre"] as? String
|
|
67
|
+
let chart = options["chart"] as? String
|
|
68
|
+
let result = try await service.getCharts(types: types, options: searchOptions, genre: genre, chart: chart)
|
|
69
|
+
return BridgeResponses.catalogCharts(result)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static func getCatalogResources(
|
|
74
|
+
service: CatalogService,
|
|
75
|
+
type: String,
|
|
76
|
+
ids: [String]
|
|
77
|
+
) async throws -> [String: Any] {
|
|
78
|
+
try await AppleMusicBridgeError.rethrow {
|
|
79
|
+
let items = try await service.getResources(type: type, ids: ids)
|
|
80
|
+
switch type {
|
|
81
|
+
case "songs":
|
|
82
|
+
return BridgeResponses.songs(items)
|
|
83
|
+
case "albums":
|
|
84
|
+
return BridgeResponses.albums(items)
|
|
85
|
+
case "artists":
|
|
86
|
+
return BridgeResponses.artists(items)
|
|
87
|
+
case "playlists":
|
|
88
|
+
return BridgeResponses.playlists(items)
|
|
89
|
+
case "stations":
|
|
90
|
+
return BridgeResponses.stations(items)
|
|
91
|
+
case "music-videos":
|
|
92
|
+
return BridgeResponses.musicVideos(items)
|
|
93
|
+
default:
|
|
94
|
+
throw CatalogService.CatalogServiceError.unknownResourceType(type)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
@available(iOS 16.0, *)
|
|
4
|
+
enum ExpoBridgeHistory {
|
|
5
|
+
static func getRecentlyPlayedResources(
|
|
6
|
+
service: HistoryService,
|
|
7
|
+
musicUserToken: String
|
|
8
|
+
) async throws -> [String: Any] {
|
|
9
|
+
try await AppleMusicBridgeError.rethrow {
|
|
10
|
+
let tracks = try await service.getRecentlyPlayedResources(musicUserToken: musicUserToken)
|
|
11
|
+
return BridgeResponses.recentlyPlayedResources(tracks)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static func getRecentlyPlayedTracks(
|
|
16
|
+
service: HistoryService,
|
|
17
|
+
musicUserToken: String,
|
|
18
|
+
options: NSDictionary
|
|
19
|
+
) async throws -> [String: Any] {
|
|
20
|
+
try await AppleMusicBridgeError.rethrow {
|
|
21
|
+
let pagination = BridgePagination(from: options)
|
|
22
|
+
let songs = try await service.getRecentlyPlayedTracks(
|
|
23
|
+
musicUserToken: musicUserToken,
|
|
24
|
+
options: pagination)
|
|
25
|
+
return BridgeResponses.songs(songs)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static func getHeavyRotation(
|
|
30
|
+
service: HistoryService,
|
|
31
|
+
musicUserToken: String,
|
|
32
|
+
options: NSDictionary
|
|
33
|
+
) async throws -> [String: Any] {
|
|
34
|
+
try await AppleMusicBridgeError.rethrow {
|
|
35
|
+
let pagination = BridgePagination(from: options)
|
|
36
|
+
let items = try await service.getHeavyRotation(
|
|
37
|
+
musicUserToken: musicUserToken,
|
|
38
|
+
limit: pagination.limit)
|
|
39
|
+
return BridgeResponses.recentItems(items)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static func getRecentlyPlayedStations(
|
|
44
|
+
service: HistoryService,
|
|
45
|
+
musicUserToken: String,
|
|
46
|
+
options: NSDictionary
|
|
47
|
+
) async throws -> [String: Any] {
|
|
48
|
+
try await AppleMusicBridgeError.rethrow {
|
|
49
|
+
let pagination = BridgePagination(from: options)
|
|
50
|
+
let stations = try await service.getRecentlyPlayedStations(
|
|
51
|
+
musicUserToken: musicUserToken,
|
|
52
|
+
limit: pagination.limit)
|
|
53
|
+
return BridgeResponses.stations(stations)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static func getRecentlyAdded(
|
|
58
|
+
service: HistoryService,
|
|
59
|
+
musicUserToken: String,
|
|
60
|
+
options: NSDictionary
|
|
61
|
+
) async throws -> [String: Any] {
|
|
62
|
+
try await AppleMusicBridgeError.rethrow {
|
|
63
|
+
let pagination = BridgePagination(from: options)
|
|
64
|
+
let items = try await service.getRecentlyAdded(
|
|
65
|
+
musicUserToken: musicUserToken,
|
|
66
|
+
limit: pagination.limit,
|
|
67
|
+
offset: pagination.offset)
|
|
68
|
+
return BridgeResponses.recentItems(items)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|