@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,201 @@
|
|
|
1
|
+
// PlaybackController.swift
|
|
2
|
+
// Encapsulates ApplicationMusicPlayer operations with caching for song info.
|
|
3
|
+
|
|
4
|
+
import AVFoundation
|
|
5
|
+
import Foundation
|
|
6
|
+
import MusicKit
|
|
7
|
+
|
|
8
|
+
@available(iOS 16.0, *)
|
|
9
|
+
final class PlaybackController {
|
|
10
|
+
|
|
11
|
+
// MARK: - Shared Instance
|
|
12
|
+
|
|
13
|
+
static let shared = PlaybackController()
|
|
14
|
+
|
|
15
|
+
// MARK: - Properties
|
|
16
|
+
|
|
17
|
+
private var player: ApplicationMusicPlayer {
|
|
18
|
+
ApplicationMusicPlayer.shared
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var state: MusicKit.MusicPlayer.State {
|
|
22
|
+
player.state
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var playbackTime: TimeInterval {
|
|
26
|
+
get { player.playbackTime }
|
|
27
|
+
set { player.playbackTime = newValue }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var currentEntry: ApplicationMusicPlayer.Queue.Entry? {
|
|
31
|
+
player.queue.currentEntry
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// MARK: - Song Info Cache
|
|
35
|
+
|
|
36
|
+
private var cachedSongId: String?
|
|
37
|
+
private var cachedSongInfo: [String: Any]?
|
|
38
|
+
|
|
39
|
+
private lazy var catalogService = CatalogService()
|
|
40
|
+
|
|
41
|
+
// MARK: - Initialization
|
|
42
|
+
|
|
43
|
+
private init() {}
|
|
44
|
+
|
|
45
|
+
/// Clears the song info cache (call when queue changes significantly)
|
|
46
|
+
func clearSongCache() {
|
|
47
|
+
cachedSongId = nil
|
|
48
|
+
cachedSongInfo = nil
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// MARK: - Audio Session Configuration
|
|
52
|
+
|
|
53
|
+
func configureAudioSession(mixWithOthers: Bool) throws {
|
|
54
|
+
let session = AVAudioSession.sharedInstance()
|
|
55
|
+
if mixWithOthers {
|
|
56
|
+
try session.setCategory(.playback, mode: .default, options: [.mixWithOthers, .duckOthers])
|
|
57
|
+
} else {
|
|
58
|
+
try session.setCategory(.playback, mode: .default)
|
|
59
|
+
}
|
|
60
|
+
try session.setActive(true)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MARK: - Playback Controls
|
|
64
|
+
|
|
65
|
+
func play() async throws {
|
|
66
|
+
try await player.play()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func pause() {
|
|
70
|
+
player.pause()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func togglePlayback() async throws {
|
|
74
|
+
switch state.playbackStatus {
|
|
75
|
+
case .playing:
|
|
76
|
+
pause()
|
|
77
|
+
case .paused, .stopped, .interrupted:
|
|
78
|
+
try await play()
|
|
79
|
+
default:
|
|
80
|
+
try await play()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func skipToNext() async throws {
|
|
85
|
+
try await player.skipToNextEntry()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func skipToPrevious() async throws {
|
|
89
|
+
try await player.skipToPreviousEntry()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func restartCurrentEntry() {
|
|
93
|
+
player.restartCurrentEntry()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func seek(to time: TimeInterval) {
|
|
97
|
+
playbackTime = time
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// MARK: - Queue Management
|
|
101
|
+
|
|
102
|
+
func setQueue<T: PlayableMusicItem>(_ item: T) async throws {
|
|
103
|
+
player.queue = [item]
|
|
104
|
+
try await player.prepareToPlay()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func setQueue<T: PlayableMusicItem>(_ items: [T], startingAt item: T) async throws {
|
|
108
|
+
player.queue = ApplicationMusicPlayer.Queue(for: items, startingAt: item)
|
|
109
|
+
try await player.prepareToPlay()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// MARK: - Current Song Info
|
|
113
|
+
|
|
114
|
+
/// Fetches detailed info for the current queue entry from the catalog.
|
|
115
|
+
/// Uses caching to avoid redundant network calls when the song hasn't changed.
|
|
116
|
+
func fetchCurrentSongInfo() async -> [String: Any]? {
|
|
117
|
+
guard let entry = currentEntry else {
|
|
118
|
+
// No current entry - clear cache and return nil
|
|
119
|
+
clearSongCache()
|
|
120
|
+
return nil
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract the current item's ID
|
|
124
|
+
let currentId: String?
|
|
125
|
+
switch entry.item {
|
|
126
|
+
case .song(let song):
|
|
127
|
+
let idString = String(describing: song.id)
|
|
128
|
+
// Skip if ID is empty (identifiers not yet resolved)
|
|
129
|
+
currentId = idString.isEmpty ? nil : idString
|
|
130
|
+
|
|
131
|
+
case .musicVideo(let musicVideo):
|
|
132
|
+
let idString = String(describing: musicVideo.id)
|
|
133
|
+
currentId = idString.isEmpty ? nil : idString
|
|
134
|
+
|
|
135
|
+
default:
|
|
136
|
+
currentId = nil
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// If no valid ID, return cached info (if any) or nil
|
|
140
|
+
guard let currentId = currentId else {
|
|
141
|
+
return cachedSongInfo
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Return cached info if same song
|
|
145
|
+
if currentId == cachedSongId, let cached = cachedSongInfo {
|
|
146
|
+
return cached
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Prefer metadata from the queue entry (avoids catalog re-fetch by cloud playback ids).
|
|
150
|
+
let songInfo: [String: Any]?
|
|
151
|
+
switch entry.item {
|
|
152
|
+
case .song(let song):
|
|
153
|
+
songInfo = await songInfoForQueueEntry(song)
|
|
154
|
+
|
|
155
|
+
case .musicVideo(let musicVideo):
|
|
156
|
+
songInfo = await musicVideoInfoForQueueEntry(musicVideo)
|
|
157
|
+
|
|
158
|
+
default:
|
|
159
|
+
songInfo = nil
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Update cache only if we got valid info
|
|
163
|
+
if let songInfo = songInfo {
|
|
164
|
+
cachedSongId = currentId
|
|
165
|
+
cachedSongInfo = songInfo
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return songInfo ?? cachedSongInfo
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private func songInfoForQueueEntry(_ song: Song) async -> [String: Any] {
|
|
172
|
+
let mapped = MusicItemMapper.map(song)
|
|
173
|
+
if hasDisplayMetadata(mapped) {
|
|
174
|
+
return mapped
|
|
175
|
+
}
|
|
176
|
+
return await fetchSongDetailsFallback(song.id) ?? mapped
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private func musicVideoInfoForQueueEntry(_ musicVideo: MusicVideo) async -> [String: Any] {
|
|
180
|
+
let mapped = MusicItemMapper.map(musicVideo)
|
|
181
|
+
if hasDisplayMetadata(mapped) {
|
|
182
|
+
return mapped
|
|
183
|
+
}
|
|
184
|
+
return await fetchMusicVideoDetailsFallback(musicVideo.id) ?? mapped
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private func hasDisplayMetadata(_ mapped: [String: Any]) -> Bool {
|
|
188
|
+
let title = mapped["title"] as? String ?? ""
|
|
189
|
+
return !title.isEmpty
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func fetchSongDetailsFallback(_ id: MusicItemID) async -> [String: Any]? {
|
|
193
|
+
guard let song = try? await catalogService.fetchSong(id: id) else { return nil }
|
|
194
|
+
return MusicItemMapper.map(song)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private func fetchMusicVideoDetailsFallback(_ id: MusicItemID) async -> [String: Any]? {
|
|
198
|
+
guard let video = try? await catalogService.fetchMusicVideo(id: id) else { return nil }
|
|
199
|
+
return MusicItemMapper.map(video)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// PlaybackObserver.swift
|
|
2
|
+
// Modern Swift Concurrency-based observation for playback state and time updates.
|
|
3
|
+
// Performance-optimized: network calls run off main thread, only UI updates on MainActor.
|
|
4
|
+
|
|
5
|
+
import Combine
|
|
6
|
+
import Foundation
|
|
7
|
+
import MusicKit
|
|
8
|
+
|
|
9
|
+
/// Protocol for receiving playback observation events.
|
|
10
|
+
@available(iOS 16.0, *)
|
|
11
|
+
protocol PlaybackObserverDelegate: AnyObject {
|
|
12
|
+
@MainActor func playbackStateDidChange(_ state: PlaybackObserver.PlaybackInfo)
|
|
13
|
+
@MainActor func currentSongDidChange(_ songInfo: [String: Any]?)
|
|
14
|
+
@MainActor func playbackTimeDidUpdate(_ time: TimeInterval)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@available(iOS 16.0, *)
|
|
18
|
+
final class PlaybackObserver {
|
|
19
|
+
|
|
20
|
+
// MARK: - Types
|
|
21
|
+
|
|
22
|
+
struct PlaybackInfo {
|
|
23
|
+
let playbackStatus: String
|
|
24
|
+
let playbackRate: Float
|
|
25
|
+
let playbackTime: TimeInterval
|
|
26
|
+
let currentSong: [String: Any]?
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// MARK: - Properties
|
|
30
|
+
|
|
31
|
+
weak var delegate: PlaybackObserverDelegate?
|
|
32
|
+
|
|
33
|
+
private let playbackController: PlaybackController
|
|
34
|
+
private var stateObservationTask: Task<Void, Never>?
|
|
35
|
+
private var queueObservationTask: Task<Void, Never>?
|
|
36
|
+
private var timeUpdateTask: Task<Void, Never>?
|
|
37
|
+
|
|
38
|
+
/// Thread-safe access to last reported status using actor isolation
|
|
39
|
+
private let statusTracker = StatusTracker()
|
|
40
|
+
|
|
41
|
+
/// Time update interval - 1 second provides good UX with less main thread pressure
|
|
42
|
+
private let timeUpdateInterval: TimeInterval = 1.0
|
|
43
|
+
|
|
44
|
+
// MARK: - Initialization
|
|
45
|
+
|
|
46
|
+
init(playbackController: PlaybackController = .shared) {
|
|
47
|
+
self.playbackController = playbackController
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
deinit {
|
|
51
|
+
stopObserving()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - Observation Lifecycle
|
|
55
|
+
|
|
56
|
+
func startObserving() {
|
|
57
|
+
startStateObservation()
|
|
58
|
+
startQueueObservation()
|
|
59
|
+
// Start time updates if already playing
|
|
60
|
+
if playbackController.state.playbackStatus == .playing {
|
|
61
|
+
startTimeUpdates()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func stopObserving() {
|
|
66
|
+
stateObservationTask?.cancel()
|
|
67
|
+
queueObservationTask?.cancel()
|
|
68
|
+
timeUpdateTask?.cancel()
|
|
69
|
+
stateObservationTask = nil
|
|
70
|
+
queueObservationTask = nil
|
|
71
|
+
timeUpdateTask = nil
|
|
72
|
+
// Note: statusTracker doesn't need explicit reset - it will be deallocated with self
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// MARK: - State Observation (Swift Concurrency)
|
|
76
|
+
|
|
77
|
+
private func startStateObservation() {
|
|
78
|
+
stateObservationTask?.cancel()
|
|
79
|
+
|
|
80
|
+
// Capture dependencies outside the task to avoid capturing self
|
|
81
|
+
let playbackController = self.playbackController
|
|
82
|
+
let statusTracker = self.statusTracker
|
|
83
|
+
weak var weakDelegate = self.delegate
|
|
84
|
+
weak var weakSelf = self
|
|
85
|
+
|
|
86
|
+
stateObservationTask = Task.detached {
|
|
87
|
+
// Use AsyncStream to bridge objectWillChange
|
|
88
|
+
let stateStream = AsyncStream<Void> { continuation in
|
|
89
|
+
let cancellable = ApplicationMusicPlayer.shared.state.objectWillChange.sink { _ in
|
|
90
|
+
continuation.yield()
|
|
91
|
+
}
|
|
92
|
+
continuation.onTermination = { _ in
|
|
93
|
+
cancellable.cancel()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for await _ in stateStream {
|
|
98
|
+
guard !Task.isCancelled else { break }
|
|
99
|
+
|
|
100
|
+
let state = playbackController.state
|
|
101
|
+
let currentStatus = state.playbackStatus
|
|
102
|
+
|
|
103
|
+
// Check if status actually changed (thread-safe via actor)
|
|
104
|
+
let shouldEmit = await statusTracker.updateIfChanged(currentStatus)
|
|
105
|
+
guard shouldEmit, !Task.isCancelled else { continue }
|
|
106
|
+
|
|
107
|
+
// Fetch song info on background thread
|
|
108
|
+
let songInfo = await playbackController.fetchCurrentSongInfo()
|
|
109
|
+
guard !Task.isCancelled else { break }
|
|
110
|
+
|
|
111
|
+
let info = PlaybackInfo(
|
|
112
|
+
playbackStatus: MusicItemMapper.describePlaybackStatus(currentStatus),
|
|
113
|
+
playbackRate: state.playbackRate,
|
|
114
|
+
playbackTime: playbackController.playbackTime,
|
|
115
|
+
currentSong: songInfo
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// Only hop to main thread for the UI callback
|
|
119
|
+
let delegate = weakDelegate
|
|
120
|
+
let observer = weakSelf
|
|
121
|
+
await MainActor.run {
|
|
122
|
+
delegate?.playbackStateDidChange(info)
|
|
123
|
+
|
|
124
|
+
// Manage time updates based on playback state
|
|
125
|
+
if currentStatus == .playing {
|
|
126
|
+
observer?.startTimeUpdates()
|
|
127
|
+
} else {
|
|
128
|
+
observer?.stopTimeUpdates()
|
|
129
|
+
let time = playbackController.playbackTime
|
|
130
|
+
delegate?.playbackTimeDidUpdate(time.isNaN ? 0 : time)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private func startQueueObservation() {
|
|
138
|
+
queueObservationTask?.cancel()
|
|
139
|
+
|
|
140
|
+
// Capture dependencies outside the task to avoid capturing self
|
|
141
|
+
let playbackController = self.playbackController
|
|
142
|
+
weak var weakDelegate = self.delegate
|
|
143
|
+
|
|
144
|
+
queueObservationTask = Task.detached {
|
|
145
|
+
let queueStream = AsyncStream<Void> { continuation in
|
|
146
|
+
let cancellable = ApplicationMusicPlayer.shared.queue.objectWillChange.sink { _ in
|
|
147
|
+
continuation.yield()
|
|
148
|
+
}
|
|
149
|
+
continuation.onTermination = { _ in
|
|
150
|
+
cancellable.cancel()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for await _ in queueStream {
|
|
155
|
+
guard !Task.isCancelled else { break }
|
|
156
|
+
|
|
157
|
+
// Small delay to let queue settle (on background thread)
|
|
158
|
+
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
|
159
|
+
guard !Task.isCancelled else { break }
|
|
160
|
+
|
|
161
|
+
// Fetch song info on background thread
|
|
162
|
+
let songInfo = await playbackController.fetchCurrentSongInfo()
|
|
163
|
+
guard !Task.isCancelled else { break }
|
|
164
|
+
|
|
165
|
+
// Only hop to main thread for the UI callback
|
|
166
|
+
let delegate = weakDelegate
|
|
167
|
+
await MainActor.run {
|
|
168
|
+
delegate?.currentSongDidChange(songInfo)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// MARK: - Time Updates (Detached Task-based Timer)
|
|
175
|
+
|
|
176
|
+
private func startTimeUpdates() {
|
|
177
|
+
guard timeUpdateTask == nil else { return }
|
|
178
|
+
|
|
179
|
+
// Capture dependencies outside the task to avoid capturing self
|
|
180
|
+
let playbackController = self.playbackController
|
|
181
|
+
let timeUpdateInterval = self.timeUpdateInterval
|
|
182
|
+
weak var weakDelegate = self.delegate
|
|
183
|
+
|
|
184
|
+
timeUpdateTask = Task.detached {
|
|
185
|
+
while !Task.isCancelled {
|
|
186
|
+
let time = playbackController.playbackTime
|
|
187
|
+
let safeTime = time.isNaN ? 0 : time
|
|
188
|
+
|
|
189
|
+
// Minimal main thread work - just the delegate call
|
|
190
|
+
let delegate = weakDelegate
|
|
191
|
+
await MainActor.run {
|
|
192
|
+
delegate?.playbackTimeDidUpdate(safeTime)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try? await Task.sleep(nanoseconds: UInt64(timeUpdateInterval * 1_000_000_000))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private func stopTimeUpdates() {
|
|
201
|
+
timeUpdateTask?.cancel()
|
|
202
|
+
timeUpdateTask = nil
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// MARK: - Status Tracker Actor
|
|
207
|
+
|
|
208
|
+
/// Thread-safe tracker for last reported playback status
|
|
209
|
+
@available(iOS 16.0, *)
|
|
210
|
+
private actor StatusTracker {
|
|
211
|
+
private var lastReportedStatus: MusicPlayer.PlaybackStatus?
|
|
212
|
+
|
|
213
|
+
/// Returns true if the status changed and was updated
|
|
214
|
+
func updateIfChanged(_ newStatus: MusicPlayer.PlaybackStatus) -> Bool {
|
|
215
|
+
if lastReportedStatus != newStatus {
|
|
216
|
+
lastReportedStatus = newStatus
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
func reset() {
|
|
223
|
+
lastReportedStatus = nil
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// QueueService.swift
|
|
2
|
+
// Management for both catalog and library items.
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MusicKit
|
|
6
|
+
|
|
7
|
+
@available(iOS 16.0, *)
|
|
8
|
+
final class QueueService {
|
|
9
|
+
|
|
10
|
+
// MARK: - Dependencies
|
|
11
|
+
|
|
12
|
+
private let playbackController: PlaybackController
|
|
13
|
+
private let catalogService: CatalogService
|
|
14
|
+
|
|
15
|
+
private func makeLibraryService() -> LibraryService {
|
|
16
|
+
LibraryService()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// MARK: - Initialization
|
|
20
|
+
|
|
21
|
+
init(
|
|
22
|
+
playbackController: PlaybackController = .shared,
|
|
23
|
+
catalogService: CatalogService = CatalogService()
|
|
24
|
+
) {
|
|
25
|
+
self.playbackController = playbackController
|
|
26
|
+
self.catalogService = catalogService
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// MARK: - Queue Setup
|
|
30
|
+
|
|
31
|
+
enum MediaType: String {
|
|
32
|
+
case song
|
|
33
|
+
case album
|
|
34
|
+
case playlist
|
|
35
|
+
case station
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func setQueue(itemId: String, type: String) async throws {
|
|
39
|
+
let musicItemId = MusicItemID(itemId)
|
|
40
|
+
let isLibrary = Self.isLibraryId(itemId)
|
|
41
|
+
|
|
42
|
+
guard let mediaType = MediaType(rawValue: type) else {
|
|
43
|
+
throw QueueServiceError.unknownMediaType(type)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if isLibrary {
|
|
47
|
+
try await setLibraryQueue(id: musicItemId, type: mediaType)
|
|
48
|
+
} else {
|
|
49
|
+
try await setCatalogQueue(id: musicItemId, type: mediaType)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// MARK: - Catalog Queue
|
|
54
|
+
|
|
55
|
+
private func setCatalogQueue(id: MusicItemID, type: MediaType) async throws {
|
|
56
|
+
switch type {
|
|
57
|
+
case .song:
|
|
58
|
+
guard let song = try await catalogService.fetchSong(id: id) else {
|
|
59
|
+
throw QueueServiceError.itemNotFound("Song", inLibrary: false)
|
|
60
|
+
}
|
|
61
|
+
try await playbackController.setQueue(song)
|
|
62
|
+
|
|
63
|
+
case .album:
|
|
64
|
+
guard let album = try await catalogService.fetchAlbum(id: id) else {
|
|
65
|
+
throw QueueServiceError.itemNotFound("Album", inLibrary: false)
|
|
66
|
+
}
|
|
67
|
+
try await playbackController.setQueue(album)
|
|
68
|
+
|
|
69
|
+
case .playlist:
|
|
70
|
+
guard let playlist = try await catalogService.fetchPlaylist(id: id) else {
|
|
71
|
+
throw QueueServiceError.itemNotFound("Playlist", inLibrary: false)
|
|
72
|
+
}
|
|
73
|
+
try await playbackController.setQueue(playlist)
|
|
74
|
+
|
|
75
|
+
case .station:
|
|
76
|
+
guard let station = try await catalogService.fetchStation(id: id) else {
|
|
77
|
+
throw QueueServiceError.itemNotFound("Station", inLibrary: false)
|
|
78
|
+
}
|
|
79
|
+
try await playbackController.setQueue(station)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// MARK: - Library Queue
|
|
84
|
+
|
|
85
|
+
private func setLibraryQueue(id: MusicItemID, type: MediaType) async throws {
|
|
86
|
+
let service = makeLibraryService()
|
|
87
|
+
|
|
88
|
+
switch type {
|
|
89
|
+
case .song:
|
|
90
|
+
guard let song = try await service.fetchSong(id: id) else {
|
|
91
|
+
throw QueueServiceError.itemNotFound("Song", inLibrary: true)
|
|
92
|
+
}
|
|
93
|
+
try await playbackController.setQueue(song)
|
|
94
|
+
|
|
95
|
+
case .album:
|
|
96
|
+
guard let album = try await service.fetchAlbum(id: id) else {
|
|
97
|
+
throw QueueServiceError.itemNotFound("Album", inLibrary: true)
|
|
98
|
+
}
|
|
99
|
+
try await playbackController.setQueue(album)
|
|
100
|
+
|
|
101
|
+
case .playlist:
|
|
102
|
+
guard let playlist = try await service.fetchPlaylist(id: id) else {
|
|
103
|
+
throw QueueServiceError.itemNotFound("Playlist", inLibrary: true)
|
|
104
|
+
}
|
|
105
|
+
try await playbackController.setQueue(playlist)
|
|
106
|
+
|
|
107
|
+
case .station:
|
|
108
|
+
// Stations are typically not in the library
|
|
109
|
+
throw QueueServiceError.unsupportedLibraryType("station")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// MARK: - Library Playback with Starting Position
|
|
114
|
+
|
|
115
|
+
func playLibrarySong(musicUserToken: String, songId: String) async throws {
|
|
116
|
+
let service = makeLibraryService()
|
|
117
|
+
let id = MusicItemID(songId)
|
|
118
|
+
guard let song = try await service.fetchSong(id: id) else {
|
|
119
|
+
throw QueueServiceError.itemNotFound("Song", inLibrary: true)
|
|
120
|
+
}
|
|
121
|
+
try await playbackController.setQueue(song)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func playLibraryPlaylist(musicUserToken: String, playlistId: String, startingAt index: Int) async throws {
|
|
125
|
+
let service = makeLibraryService()
|
|
126
|
+
let id = MusicItemID(playlistId)
|
|
127
|
+
guard let playlist = try await service.fetchPlaylist(id: id) else {
|
|
128
|
+
throw QueueServiceError.itemNotFound("Playlist", inLibrary: true)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let songs = try await service.extractSongs(from: playlist)
|
|
132
|
+
guard !songs.isEmpty else {
|
|
133
|
+
throw LibraryServiceError.noSongsInPlaylist
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let startIndex = (index >= 0 && index < songs.count) ? index : 0
|
|
137
|
+
try await playbackController.setQueue(songs, startingAt: songs[startIndex])
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// MARK: - Helpers
|
|
141
|
+
|
|
142
|
+
/// Checks if an ID is a library ID (starts with "l.", "i.", or "p.")
|
|
143
|
+
static func isLibraryId(_ itemId: String) -> Bool {
|
|
144
|
+
itemId.hasPrefix("l.") || itemId.hasPrefix("i.") || itemId.hasPrefix("p.")
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// MARK: - Errors
|
|
149
|
+
|
|
150
|
+
enum QueueServiceError: LocalizedError {
|
|
151
|
+
case unknownMediaType(String)
|
|
152
|
+
case itemNotFound(String, inLibrary: Bool)
|
|
153
|
+
case unsupportedLibraryType(String)
|
|
154
|
+
|
|
155
|
+
var errorDescription: String? {
|
|
156
|
+
switch self {
|
|
157
|
+
case .unknownMediaType(let type):
|
|
158
|
+
return "Unknown media type: \(type)"
|
|
159
|
+
case .itemNotFound(let item, let inLibrary):
|
|
160
|
+
let source = inLibrary ? "library" : "catalog"
|
|
161
|
+
return "\(item) not found in \(source)"
|
|
162
|
+
case .unsupportedLibraryType(let type):
|
|
163
|
+
return "Unsupported library media type: \(type)"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// RatingsService.swift
|
|
2
|
+
// Personal ratings and favorites via Apple Music REST API.
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
|
|
6
|
+
@available(iOS 16.0, *)
|
|
7
|
+
final class RatingsService {
|
|
8
|
+
|
|
9
|
+
func getRating(musicUserToken: String, resourceType: String, id: String) async throws -> [String: Any]? {
|
|
10
|
+
do {
|
|
11
|
+
let json = try await AppleMusicRestClient.get(
|
|
12
|
+
path: "/v1/me/ratings/\(resourceType)/\(id)",
|
|
13
|
+
musicUserToken: musicUserToken
|
|
14
|
+
)
|
|
15
|
+
return RestJsonMapper.mapRating(json)
|
|
16
|
+
} catch let error as AppleMusicRestClient.RestError {
|
|
17
|
+
if case .apiError(let message) = error, message.contains("(404)") {
|
|
18
|
+
return nil
|
|
19
|
+
}
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func setRating(musicUserToken: String, resourceType: String, id: String, value: Int) async throws -> [String: Any] {
|
|
25
|
+
let body: [String: Any] = [
|
|
26
|
+
"type": "rating",
|
|
27
|
+
"attributes": ["value": value],
|
|
28
|
+
]
|
|
29
|
+
let json = try await AppleMusicRestClient.request(
|
|
30
|
+
method: .put,
|
|
31
|
+
path: "/v1/me/ratings/\(resourceType)/\(id)",
|
|
32
|
+
musicUserToken: musicUserToken,
|
|
33
|
+
body: body
|
|
34
|
+
)
|
|
35
|
+
guard let rating = RestJsonMapper.mapRating(json) else {
|
|
36
|
+
throw AppleMusicRestClient.RestError.invalidResponse
|
|
37
|
+
}
|
|
38
|
+
return rating
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func clearRating(musicUserToken: String, resourceType: String, id: String) async throws {
|
|
42
|
+
_ = try await AppleMusicRestClient.request(
|
|
43
|
+
method: .delete,
|
|
44
|
+
path: "/v1/me/ratings/\(resourceType)/\(id)",
|
|
45
|
+
musicUserToken: musicUserToken
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func addToFavorites(musicUserToken: String, resourceIds: [String: [String]]) async throws {
|
|
50
|
+
_ = try await AppleMusicRestClient.request(
|
|
51
|
+
method: .post,
|
|
52
|
+
path: "/v1/me/favorites",
|
|
53
|
+
musicUserToken: musicUserToken,
|
|
54
|
+
query: RestJsonMapper.buildIdsQuery(resourceIds)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func removeFromFavorites(musicUserToken: String, resourceIds: [String: [String]]) async throws {
|
|
59
|
+
_ = try await AppleMusicRestClient.request(
|
|
60
|
+
method: .delete,
|
|
61
|
+
path: "/v1/me/favorites",
|
|
62
|
+
musicUserToken: musicUserToken,
|
|
63
|
+
query: RestJsonMapper.buildIdsQuery(resourceIds)
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// RecommendationsService.swift
|
|
2
|
+
// Personal recommendations and Replay via Apple Music REST.
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
|
|
6
|
+
@available(iOS 16.0, *)
|
|
7
|
+
final class RecommendationsService {
|
|
8
|
+
|
|
9
|
+
func getRecommendations(musicUserToken: String, ids: [String]?) async throws -> [[String: Any]] {
|
|
10
|
+
var query: [String: String] = [:]
|
|
11
|
+
if let ids, !ids.isEmpty {
|
|
12
|
+
query["ids"] = ids.joined(separator: ",")
|
|
13
|
+
}
|
|
14
|
+
let data = try await AppleMusicRestClient.getDataArray(
|
|
15
|
+
path: "/v1/me/recommendations",
|
|
16
|
+
musicUserToken: musicUserToken,
|
|
17
|
+
query: query
|
|
18
|
+
)
|
|
19
|
+
return data.map(RestJsonMapper.mapRecommendation)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func getReplay(musicUserToken: String, year: Int?) async throws -> [[String: Any]] {
|
|
23
|
+
var query: [String: String] = [:]
|
|
24
|
+
if let year {
|
|
25
|
+
query["filter[year]"] = "\(year)"
|
|
26
|
+
}
|
|
27
|
+
let data = try await AppleMusicRestClient.getDataArray(
|
|
28
|
+
path: "/v1/me/music-summaries",
|
|
29
|
+
musicUserToken: musicUserToken,
|
|
30
|
+
query: query
|
|
31
|
+
)
|
|
32
|
+
return data.map(RestJsonMapper.mapReplaySummary)
|
|
33
|
+
}
|
|
34
|
+
}
|