@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,484 @@
|
|
|
1
|
+
package expo.modules.applemusic
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import com.apple.android.music.playback.controller.MediaPlayerController
|
|
8
|
+
import com.apple.android.music.playback.controller.MediaPlayerControllerFactory
|
|
9
|
+
import com.apple.android.music.playback.model.MediaContainerType
|
|
10
|
+
import com.apple.android.music.playback.model.MediaItemType
|
|
11
|
+
import com.apple.android.music.playback.model.MediaPlayerException
|
|
12
|
+
import com.apple.android.music.playback.model.PlayerMediaItem
|
|
13
|
+
import com.apple.android.music.playback.model.PlayerQueueItem
|
|
14
|
+
import com.apple.android.music.playback.queue.CatalogPlaybackQueueItemProvider
|
|
15
|
+
import com.apple.android.music.playback.queue.PlaybackQueueInsertionType
|
|
16
|
+
import com.apple.android.music.playback.queue.PlaybackQueueItemProvider
|
|
17
|
+
import expo.modules.kotlin.exception.CodedException
|
|
18
|
+
import kotlinx.coroutines.Dispatchers
|
|
19
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
20
|
+
import kotlinx.coroutines.withContext
|
|
21
|
+
import kotlin.coroutines.resume
|
|
22
|
+
import kotlin.coroutines.resumeWithException
|
|
23
|
+
|
|
24
|
+
internal class AndroidPlaybackController private constructor(
|
|
25
|
+
context: Context,
|
|
26
|
+
) {
|
|
27
|
+
private val appContext = context.applicationContext
|
|
28
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
29
|
+
|
|
30
|
+
@Volatile
|
|
31
|
+
private var controller: MediaPlayerController? = null
|
|
32
|
+
|
|
33
|
+
private val externalListeners =
|
|
34
|
+
mutableSetOf<MediaPlayerController.Listener>()
|
|
35
|
+
|
|
36
|
+
@Volatile
|
|
37
|
+
private var boundMusicUserToken: String? = null
|
|
38
|
+
|
|
39
|
+
/** Drops the native player when the music user token changes (SDK caches credentials). */
|
|
40
|
+
internal fun applyMusicUserToken(token: String?) {
|
|
41
|
+
val trimmed = token?.trim()?.takeIf { it.isNotEmpty() } ?: return
|
|
42
|
+
MusicKitAuthStorage.saveMusicUserToken(appContext, trimmed)
|
|
43
|
+
if (trimmed != boundMusicUserToken) {
|
|
44
|
+
releaseControllerSync()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private fun ensureController(): MediaPlayerController {
|
|
49
|
+
AndroidDeveloperToken.requireStored(appContext)
|
|
50
|
+
val token = MusicKitAuthStorage.getMusicUserToken(appContext)
|
|
51
|
+
if (!token.isNullOrEmpty()) {
|
|
52
|
+
return ensurePlaybackController(token)
|
|
53
|
+
}
|
|
54
|
+
val existing = controller
|
|
55
|
+
if (existing != null) {
|
|
56
|
+
return existing
|
|
57
|
+
}
|
|
58
|
+
return createController(null)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun ensurePlaybackController(musicUserToken: String): MediaPlayerController {
|
|
62
|
+
val existing = controller
|
|
63
|
+
if (existing != null && boundMusicUserToken == musicUserToken) {
|
|
64
|
+
return existing
|
|
65
|
+
}
|
|
66
|
+
releaseControllerSync()
|
|
67
|
+
return createController(musicUserToken)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private fun createController(musicUserToken: String?): MediaPlayerController {
|
|
71
|
+
AppleMusicNativeLoader.ensureLoaded()
|
|
72
|
+
return MediaPlayerControllerFactory.createLocalController(
|
|
73
|
+
appContext,
|
|
74
|
+
MusicKitTokenProvider(appContext),
|
|
75
|
+
).also { player ->
|
|
76
|
+
player.addListener(globalErrorListener)
|
|
77
|
+
externalListeners.forEach { player.addListener(it) }
|
|
78
|
+
controller = player
|
|
79
|
+
boundMusicUserToken = musicUserToken
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun releaseControllerSync() {
|
|
84
|
+
clearSongCache()
|
|
85
|
+
val player = controller ?: return
|
|
86
|
+
controller = null
|
|
87
|
+
boundMusicUserToken = null
|
|
88
|
+
try {
|
|
89
|
+
player.removeListener(globalErrorListener)
|
|
90
|
+
externalListeners.forEach { player.removeListener(it) }
|
|
91
|
+
player.release()
|
|
92
|
+
} catch (error: Exception) {
|
|
93
|
+
Log.w(TAG, "release playback controller failed", error)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private var cachedSongId: String? = null
|
|
98
|
+
private var cachedSongInfo: Map<String, Any?>? = null
|
|
99
|
+
|
|
100
|
+
var playbackErrorHandler: ((MediaPlayerException, String) -> Unit)? = null
|
|
101
|
+
|
|
102
|
+
private val globalErrorListener =
|
|
103
|
+
object : MediaPlayerController.Listener {
|
|
104
|
+
override fun onPlaybackError(
|
|
105
|
+
player: MediaPlayerController,
|
|
106
|
+
error: MediaPlayerException,
|
|
107
|
+
) {
|
|
108
|
+
Log.e(TAG, "playback error type=${error.type} code=${error.errorCode}", error)
|
|
109
|
+
playbackErrorHandler?.invoke(error, "playback")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override fun onPlayerStateRestored(player: MediaPlayerController) {}
|
|
113
|
+
override fun onPlaybackStateChanged(
|
|
114
|
+
player: MediaPlayerController,
|
|
115
|
+
previousState: Int,
|
|
116
|
+
newState: Int,
|
|
117
|
+
) {
|
|
118
|
+
}
|
|
119
|
+
override fun onPlaybackStateUpdated(player: MediaPlayerController) {}
|
|
120
|
+
override fun onBufferingStateChanged(player: MediaPlayerController, buffering: Boolean) {}
|
|
121
|
+
override fun onCurrentItemChanged(
|
|
122
|
+
player: MediaPlayerController,
|
|
123
|
+
previous: PlayerQueueItem?,
|
|
124
|
+
current: PlayerQueueItem?,
|
|
125
|
+
) {
|
|
126
|
+
}
|
|
127
|
+
override fun onItemEnded(
|
|
128
|
+
player: MediaPlayerController,
|
|
129
|
+
item: PlayerQueueItem,
|
|
130
|
+
endPosition: Long,
|
|
131
|
+
) {
|
|
132
|
+
}
|
|
133
|
+
override fun onMetadataUpdated(player: MediaPlayerController, item: PlayerQueueItem) {}
|
|
134
|
+
override fun onPlaybackQueueChanged(
|
|
135
|
+
player: MediaPlayerController,
|
|
136
|
+
items: MutableList<PlayerQueueItem>,
|
|
137
|
+
) {
|
|
138
|
+
}
|
|
139
|
+
override fun onPlaybackQueueItemsAdded(
|
|
140
|
+
player: MediaPlayerController,
|
|
141
|
+
queueInsertionType: Int,
|
|
142
|
+
containerIndex: Int,
|
|
143
|
+
itemCount: Int,
|
|
144
|
+
) {
|
|
145
|
+
}
|
|
146
|
+
override fun onPlaybackRepeatModeChanged(player: MediaPlayerController, mode: Int) {}
|
|
147
|
+
override fun onPlaybackShuffleModeChanged(player: MediaPlayerController, mode: Int) {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fun addListener(listener: MediaPlayerController.Listener) {
|
|
151
|
+
externalListeners.add(listener)
|
|
152
|
+
val player = controller
|
|
153
|
+
if (player != null) {
|
|
154
|
+
player.addListener(listener)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
if (hasStoredDeveloperToken()) {
|
|
158
|
+
ensureController().addListener(listener)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* After [MusicKitAuthStorage.saveDeveloperToken], attach listeners registered before a JWT existed.
|
|
164
|
+
*/
|
|
165
|
+
internal fun attachPendingListenersAfterDeveloperTokenStored() {
|
|
166
|
+
if (!hasStoredDeveloperToken() || externalListeners.isEmpty()) return
|
|
167
|
+
ensureController()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private fun hasStoredDeveloperToken(): Boolean =
|
|
171
|
+
!MusicKitAuthStorage.getDeveloperToken(appContext).isNullOrBlank()
|
|
172
|
+
|
|
173
|
+
private fun idlePlaybackState(): Map<String, Any?> =
|
|
174
|
+
mapOf(
|
|
175
|
+
"playbackRate" to 1.0,
|
|
176
|
+
"playbackStatus" to "stopped",
|
|
177
|
+
"playbackTime" to 0.0,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
fun removeListener(listener: MediaPlayerController.Listener) {
|
|
181
|
+
externalListeners.remove(listener)
|
|
182
|
+
controller?.removeListener(listener)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** API parity with iOS `configureAudioSession`; playback focus is handled by [MediaPlayerController]. */
|
|
186
|
+
fun configurePlayer(mixWithOthers: Boolean): Map<String, Any?> =
|
|
187
|
+
mapOf("mixWithOthers" to mixWithOthers)
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* [MediaPlayerController.prepare] loads queue items asynchronously — wait for
|
|
191
|
+
* [onPlaybackQueueItemsAdded] before returning to JS.
|
|
192
|
+
*/
|
|
193
|
+
suspend fun prepareQueue(provider: PlaybackQueueItemProvider, musicUserToken: String? = null) {
|
|
194
|
+
AndroidDeveloperToken.requireStored(appContext)
|
|
195
|
+
val stack = AppleMusicRestStack.create(appContext)
|
|
196
|
+
val effectiveToken = resolvePlaybackMusicUserToken(appContext, musicUserToken)
|
|
197
|
+
stack.storefront.requireUserStorefront(effectiveToken)
|
|
198
|
+
withContext(Dispatchers.Main) {
|
|
199
|
+
val player = ensurePlaybackController(effectiveToken)
|
|
200
|
+
suspendCancellableCoroutine { continuation ->
|
|
201
|
+
lateinit var timeoutRunnable: Runnable
|
|
202
|
+
|
|
203
|
+
val prepareListener =
|
|
204
|
+
object : MediaPlayerController.Listener {
|
|
205
|
+
private fun cleanup() {
|
|
206
|
+
player.removeListener(this)
|
|
207
|
+
mainHandler.removeCallbacks(timeoutRunnable)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private fun finishSuccess() {
|
|
211
|
+
if (!continuation.isActive) return
|
|
212
|
+
cleanup()
|
|
213
|
+
continuation.resume(Unit)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private fun finishError(error: Exception) {
|
|
217
|
+
if (!continuation.isActive) return
|
|
218
|
+
cleanup()
|
|
219
|
+
continuation.resumeWithException(mapPlaybackException(error))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
override fun onPlaybackQueueItemsAdded(
|
|
223
|
+
player: MediaPlayerController,
|
|
224
|
+
queueInsertionType: Int,
|
|
225
|
+
containerIndex: Int,
|
|
226
|
+
itemCount: Int,
|
|
227
|
+
) {
|
|
228
|
+
if (itemCount > 0) finishSuccess()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
override fun onPlaybackError(
|
|
232
|
+
player: MediaPlayerController,
|
|
233
|
+
error: MediaPlayerException,
|
|
234
|
+
) {
|
|
235
|
+
finishError(error)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
override fun onPlayerStateRestored(player: MediaPlayerController) {}
|
|
239
|
+
override fun onPlaybackStateChanged(
|
|
240
|
+
player: MediaPlayerController,
|
|
241
|
+
previousState: Int,
|
|
242
|
+
newState: Int,
|
|
243
|
+
) {
|
|
244
|
+
}
|
|
245
|
+
override fun onPlaybackStateUpdated(player: MediaPlayerController) {}
|
|
246
|
+
override fun onBufferingStateChanged(player: MediaPlayerController, buffering: Boolean) {}
|
|
247
|
+
override fun onCurrentItemChanged(
|
|
248
|
+
player: MediaPlayerController,
|
|
249
|
+
previous: PlayerQueueItem?,
|
|
250
|
+
current: PlayerQueueItem?,
|
|
251
|
+
) {
|
|
252
|
+
}
|
|
253
|
+
override fun onItemEnded(
|
|
254
|
+
player: MediaPlayerController,
|
|
255
|
+
item: PlayerQueueItem,
|
|
256
|
+
endPosition: Long,
|
|
257
|
+
) {
|
|
258
|
+
}
|
|
259
|
+
override fun onMetadataUpdated(player: MediaPlayerController, item: PlayerQueueItem) {}
|
|
260
|
+
override fun onPlaybackQueueChanged(
|
|
261
|
+
player: MediaPlayerController,
|
|
262
|
+
items: MutableList<PlayerQueueItem>,
|
|
263
|
+
) {
|
|
264
|
+
}
|
|
265
|
+
override fun onPlaybackRepeatModeChanged(player: MediaPlayerController, mode: Int) {}
|
|
266
|
+
override fun onPlaybackShuffleModeChanged(player: MediaPlayerController, mode: Int) {}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
timeoutRunnable =
|
|
270
|
+
Runnable {
|
|
271
|
+
if (!continuation.isActive) return@Runnable
|
|
272
|
+
player.removeListener(prepareListener)
|
|
273
|
+
if (player.playbackQueueItemCount > 0) {
|
|
274
|
+
continuation.resume(Unit)
|
|
275
|
+
} else {
|
|
276
|
+
continuation.resumeWithException(
|
|
277
|
+
CodedException(
|
|
278
|
+
AppleMusicErrorCodes.PLAYBACK_ERROR,
|
|
279
|
+
"Playback queue stayed empty after prepare",
|
|
280
|
+
null,
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
continuation.invokeOnCancellation {
|
|
287
|
+
player.removeListener(prepareListener)
|
|
288
|
+
mainHandler.removeCallbacks(timeoutRunnable)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
player.addListener(prepareListener)
|
|
292
|
+
mainHandler.postDelayed(timeoutRunnable, PREPARE_TIMEOUT_MS)
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
player.prepare(
|
|
296
|
+
provider,
|
|
297
|
+
PlaybackQueueInsertionType.INSERTION_TYPE_CLEAR_AND_REPLACE,
|
|
298
|
+
true,
|
|
299
|
+
)
|
|
300
|
+
} catch (error: Exception) {
|
|
301
|
+
player.removeListener(prepareListener)
|
|
302
|
+
mainHandler.removeCallbacks(timeoutRunnable)
|
|
303
|
+
if (continuation.isActive) {
|
|
304
|
+
continuation.resumeWithException(mapPlaybackException(error))
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
fun play() {
|
|
312
|
+
mainHandler.post { ensureController().play() }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fun pause() {
|
|
316
|
+
mainHandler.post { ensureController().pause() }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fun togglePlayback() {
|
|
320
|
+
mainHandler.post {
|
|
321
|
+
val player = ensureController()
|
|
322
|
+
when (player.playbackState) {
|
|
323
|
+
com.apple.android.music.playback.model.PlaybackState.PLAYING -> player.pause()
|
|
324
|
+
else -> player.play()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fun skipToNext() {
|
|
330
|
+
mainHandler.post { ensureController().skipToNextItem() }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fun skipToPrevious() {
|
|
334
|
+
mainHandler.post { ensureController().skipToPreviousItem() }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fun restartCurrentEntry(onComplete: ((Double) -> Unit)? = null) {
|
|
338
|
+
mainHandler.post {
|
|
339
|
+
ensureController().seekToPosition(0)
|
|
340
|
+
onComplete?.invoke(0.0)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fun seekToTime(seconds: Double, onComplete: ((Double) -> Unit)? = null) {
|
|
345
|
+
mainHandler.post {
|
|
346
|
+
val player = ensureController()
|
|
347
|
+
player.seekToPosition((seconds * 1000).toLong())
|
|
348
|
+
val actual = player.currentPosition.coerceAtLeast(0) / 1000.0
|
|
349
|
+
onComplete?.invoke(actual)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fun buildSongProvider(vararg catalogIds: String, startIndex: Int = 0): PlaybackQueueItemProvider {
|
|
354
|
+
val ids = catalogIds.map { it.trim() }.filter { it.isNotEmpty() }.toTypedArray()
|
|
355
|
+
if (ids.isEmpty()) {
|
|
356
|
+
throw AppleMusicErrors.apiError("No catalog song ids for playback queue")
|
|
357
|
+
}
|
|
358
|
+
return CatalogPlaybackQueueItemProvider.Builder()
|
|
359
|
+
.items(MediaItemType.SONG, *ids)
|
|
360
|
+
.apply {
|
|
361
|
+
if (startIndex > 0) startItemIndex(startIndex)
|
|
362
|
+
}
|
|
363
|
+
.build()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fun buildAlbumProvider(catalogId: String): PlaybackQueueItemProvider =
|
|
367
|
+
CatalogPlaybackQueueItemProvider.Builder()
|
|
368
|
+
.containers(MediaContainerType.ALBUM, catalogId.trim())
|
|
369
|
+
.build()
|
|
370
|
+
|
|
371
|
+
fun buildPlaylistProvider(catalogId: String): PlaybackQueueItemProvider =
|
|
372
|
+
CatalogPlaybackQueueItemProvider.Builder()
|
|
373
|
+
.containers(MediaContainerType.PLAYLIST, catalogId.trim())
|
|
374
|
+
.build()
|
|
375
|
+
|
|
376
|
+
fun warmUp() {
|
|
377
|
+
AppleMusicNativeLoader.ensureLoaded()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Releases the native player and clears caches; keeps this singleton for observer re-attach. */
|
|
381
|
+
internal fun releaseMediaPlayer() {
|
|
382
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
383
|
+
releaseControllerSync()
|
|
384
|
+
} else {
|
|
385
|
+
mainHandler.post { releaseControllerSync() }
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
fun clearSongCache() {
|
|
390
|
+
cachedSongId = null
|
|
391
|
+
cachedSongInfo = null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
fun currentState(): Map<String, Any?> {
|
|
395
|
+
if (!hasStoredDeveloperToken()) {
|
|
396
|
+
return idlePlaybackState()
|
|
397
|
+
}
|
|
398
|
+
val player = controller ?: ensureController()
|
|
399
|
+
val playbackStatus = AppleMusicJsonMapper.describePlaybackStatus(player.playbackState)
|
|
400
|
+
val playbackTime = player.currentPosition.coerceAtLeast(0) / 1000.0
|
|
401
|
+
return buildMap {
|
|
402
|
+
put("playbackRate", player.playbackRate)
|
|
403
|
+
put("playbackStatus", playbackStatus)
|
|
404
|
+
put("playbackTime", playbackTime)
|
|
405
|
+
fetchCurrentSongInfo()?.let { put("currentSong", it) }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
fun fetchCurrentSongInfo(): Map<String, Any?>? {
|
|
410
|
+
val player = controller ?: return null
|
|
411
|
+
val item: PlayerMediaItem = player.currentItem?.item ?: return run {
|
|
412
|
+
clearSongCache()
|
|
413
|
+
null
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
val currentId =
|
|
417
|
+
item.subscriptionStoreId?.takeIf { it.isNotEmpty() }
|
|
418
|
+
?: item.a()?.takeIf { !it.isNullOrEmpty() }
|
|
419
|
+
|
|
420
|
+
if (currentId == null) {
|
|
421
|
+
val fallback = AppleMusicJsonMapper.mapPlayerMediaItem(item)
|
|
422
|
+
return fallback.takeIf { (it["title"] as? String)?.isNotEmpty() == true } ?: cachedSongInfo
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (currentId == cachedSongId && cachedSongInfo != null) {
|
|
426
|
+
return cachedSongInfo
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
val songInfo = AppleMusicJsonMapper.mapPlayerMediaItem(item)
|
|
430
|
+
cachedSongId = currentId
|
|
431
|
+
cachedSongInfo = songInfo
|
|
432
|
+
return songInfo
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
companion object {
|
|
437
|
+
private const val TAG = "ExpoAppleMusic"
|
|
438
|
+
private const val PREPARE_TIMEOUT_MS = 20_000L
|
|
439
|
+
|
|
440
|
+
@Volatile
|
|
441
|
+
private var instance: AndroidPlaybackController? = null
|
|
442
|
+
|
|
443
|
+
fun getInstance(context: Context): AndroidPlaybackController =
|
|
444
|
+
instance
|
|
445
|
+
?: synchronized(this) {
|
|
446
|
+
instance ?: AndroidPlaybackController(context.applicationContext).also { instance = it }
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fun warmUp(context: Context) {
|
|
450
|
+
try {
|
|
451
|
+
getInstance(context).warmUp()
|
|
452
|
+
} catch (error: Exception) {
|
|
453
|
+
Log.w(TAG, "playback warmUp failed", error)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
fun resetInstance() {
|
|
458
|
+
synchronized(this) {
|
|
459
|
+
instance?.releaseMediaPlayer()
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
fun mapPlaybackException(error: Exception): CodedException =
|
|
464
|
+
when (error) {
|
|
465
|
+
is CodedException -> error
|
|
466
|
+
is MediaPlayerException ->
|
|
467
|
+
CodedException(
|
|
468
|
+
AppleMusicErrorCodes.PLAYBACK_ERROR,
|
|
469
|
+
error.message ?: "Media playback failed (type=${error.type}, code=${error.errorCode})",
|
|
470
|
+
null,
|
|
471
|
+
)
|
|
472
|
+
else -> {
|
|
473
|
+
val message = error.message.orEmpty()
|
|
474
|
+
val hint =
|
|
475
|
+
if (error is java.io.FileNotFoundException && message.contains("api.music.apple.com")) {
|
|
476
|
+
"Apple Music API rejected the request (often an expired session). Call Auth.authorize(developerToken) again."
|
|
477
|
+
} else {
|
|
478
|
+
error.message ?: "Playback failed"
|
|
479
|
+
}
|
|
480
|
+
CodedException(AppleMusicErrorCodes.PLAYBACK_ERROR, hint, null)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
package expo.modules.applemusic
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import com.apple.android.music.playback.controller.MediaPlayerController
|
|
7
|
+
import com.apple.android.music.playback.model.MediaPlayerException
|
|
8
|
+
import com.apple.android.music.playback.model.PlayerQueueItem
|
|
9
|
+
import com.apple.android.music.playback.model.PlaybackState
|
|
10
|
+
|
|
11
|
+
internal interface AndroidPlaybackObserverDelegate {
|
|
12
|
+
fun onPlaybackStateChange(body: Map<String, Any?>)
|
|
13
|
+
fun onCurrentSongChange(body: Map<String, Any?>)
|
|
14
|
+
fun onPlaybackTimeUpdate(playbackTime: Double)
|
|
15
|
+
fun onPlaybackError(body: Map<String, Any?>)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
internal class AndroidPlaybackObserver(
|
|
19
|
+
context: Context,
|
|
20
|
+
) {
|
|
21
|
+
private val appContext = context.applicationContext
|
|
22
|
+
private val playback: AndroidPlaybackController
|
|
23
|
+
get() = AndroidPlaybackController.getInstance(appContext)
|
|
24
|
+
|
|
25
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
26
|
+
private var observing = false
|
|
27
|
+
private var lastReportedStatus: String? = null
|
|
28
|
+
private var timeRunnable: Runnable? = null
|
|
29
|
+
private var pendingSongEmitRunnable: Runnable? = null
|
|
30
|
+
|
|
31
|
+
var delegate: AndroidPlaybackObserverDelegate? = null
|
|
32
|
+
|
|
33
|
+
private val listener =
|
|
34
|
+
object : MediaPlayerController.Listener {
|
|
35
|
+
override fun onPlaybackStateChanged(
|
|
36
|
+
player: MediaPlayerController,
|
|
37
|
+
previousState: Int,
|
|
38
|
+
newState: Int,
|
|
39
|
+
) {
|
|
40
|
+
emitStateIfChanged()
|
|
41
|
+
manageTimeUpdates(newState == PlaybackState.PLAYING)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun onPlaybackStateUpdated(player: MediaPlayerController) {
|
|
45
|
+
emitStateIfChanged()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override fun onCurrentItemChanged(
|
|
49
|
+
player: MediaPlayerController,
|
|
50
|
+
previous: PlayerQueueItem?,
|
|
51
|
+
current: PlayerQueueItem?,
|
|
52
|
+
) {
|
|
53
|
+
scheduleEmitCurrentSong()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun onMetadataUpdated(player: MediaPlayerController, item: PlayerQueueItem) {
|
|
57
|
+
scheduleEmitCurrentSong()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun onPlaybackQueueItemsAdded(
|
|
61
|
+
player: MediaPlayerController,
|
|
62
|
+
queueInsertionType: Int,
|
|
63
|
+
containerIndex: Int,
|
|
64
|
+
itemCount: Int,
|
|
65
|
+
) {
|
|
66
|
+
if (itemCount > 0) scheduleEmitCurrentSong()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override fun onPlaybackError(player: MediaPlayerController, error: MediaPlayerException) {
|
|
70
|
+
delegate?.onPlaybackError(
|
|
71
|
+
mapOf(
|
|
72
|
+
"message" to (error.message ?: "Playback error"),
|
|
73
|
+
"code" to error.errorCode,
|
|
74
|
+
"domain" to "MediaPlayer",
|
|
75
|
+
"operation" to "playback",
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
override fun onPlayerStateRestored(player: MediaPlayerController) {}
|
|
81
|
+
override fun onBufferingStateChanged(player: MediaPlayerController, buffering: Boolean) {}
|
|
82
|
+
override fun onItemEnded(player: MediaPlayerController, item: PlayerQueueItem, endPosition: Long) {}
|
|
83
|
+
override fun onPlaybackQueueChanged(
|
|
84
|
+
player: MediaPlayerController,
|
|
85
|
+
items: MutableList<PlayerQueueItem>,
|
|
86
|
+
) {
|
|
87
|
+
}
|
|
88
|
+
override fun onPlaybackRepeatModeChanged(player: MediaPlayerController, mode: Int) {}
|
|
89
|
+
override fun onPlaybackShuffleModeChanged(player: MediaPlayerController, mode: Int) {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fun startObserving() {
|
|
93
|
+
if (observing) return
|
|
94
|
+
observing = true
|
|
95
|
+
playback.addListener(listener)
|
|
96
|
+
emitStateIfChanged()
|
|
97
|
+
scheduleEmitCurrentSong()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun stopObserving() {
|
|
101
|
+
if (!observing) return
|
|
102
|
+
observing = false
|
|
103
|
+
playback.removeListener(listener)
|
|
104
|
+
cancelPendingSongEmit()
|
|
105
|
+
stopTimeUpdates()
|
|
106
|
+
lastReportedStatus = null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private fun emitStateIfChanged() {
|
|
110
|
+
val state = playback.currentState()
|
|
111
|
+
val status = state["playbackStatus"] as? String ?: return
|
|
112
|
+
if (status == lastReportedStatus) return
|
|
113
|
+
lastReportedStatus = status
|
|
114
|
+
delegate?.onPlaybackStateChange(state)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private fun scheduleEmitCurrentSong() {
|
|
118
|
+
cancelPendingSongEmit()
|
|
119
|
+
emitCurrentSong(attempt = 0)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun cancelPendingSongEmit() {
|
|
123
|
+
pendingSongEmitRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
124
|
+
pendingSongEmitRunnable = null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private fun emitCurrentSong(attempt: Int) {
|
|
128
|
+
val song = playback.fetchCurrentSongInfo()
|
|
129
|
+
if (song != null) {
|
|
130
|
+
cancelPendingSongEmit()
|
|
131
|
+
delegate?.onCurrentSongChange(mapOf("currentSong" to song))
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (!observing || attempt >= 4) return
|
|
135
|
+
val runnable =
|
|
136
|
+
Runnable {
|
|
137
|
+
pendingSongEmitRunnable = null
|
|
138
|
+
emitCurrentSong(attempt + 1)
|
|
139
|
+
}
|
|
140
|
+
pendingSongEmitRunnable = runnable
|
|
141
|
+
mainHandler.postDelayed(runnable, 150L * (attempt + 1))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private fun manageTimeUpdates(playing: Boolean) {
|
|
145
|
+
if (playing) {
|
|
146
|
+
startTimeUpdates()
|
|
147
|
+
} else {
|
|
148
|
+
stopTimeUpdates()
|
|
149
|
+
val time = playback.currentState()["playbackTime"] as? Double ?: 0.0
|
|
150
|
+
delegate?.onPlaybackTimeUpdate(time)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private fun startTimeUpdates() {
|
|
155
|
+
if (timeRunnable != null) return
|
|
156
|
+
val runnable =
|
|
157
|
+
object : Runnable {
|
|
158
|
+
override fun run() {
|
|
159
|
+
if (!observing) return
|
|
160
|
+
val time = playback.currentState()["playbackTime"] as? Double ?: 0.0
|
|
161
|
+
delegate?.onPlaybackTimeUpdate(time)
|
|
162
|
+
mainHandler.postDelayed(this, 1000)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
timeRunnable = runnable
|
|
166
|
+
mainHandler.post(runnable)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private fun stopTimeUpdates() {
|
|
170
|
+
timeRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
171
|
+
timeRunnable = null
|
|
172
|
+
}
|
|
173
|
+
}
|