diff --git a/README.md b/README.md
index fbd8e8c..a43e1ba 100644
--- a/README.md
+++ b/README.md
@@ -10,27 +10,33 @@ An [Example](./example) project was developed to exercise and test all functiona
The following table shows the platform support for various Apple Music functionality within this library.
-| Feature | iOS |
-| :--------------------------- | :-: |
-| **Auth** |
-| `authorize` | ✅ |
-| `checkSubscription` | ✅ |
-| **Player** |
-| `play` | ✅ |
-| `pause` | ✅ |
-| `togglePlayerState` | ✅ |
-| `skipToNextEntry` | ✅ |
-| `getCurrentState` | ✅ |
-| `addListener` | ✅ |
-| `removeListener` | ✅ |
-| **MusicKit** |
-| `catalogSearch` | ✅ |
+| Feature | iOS |
+|:-----------------------| :-: |
+| **Auth** |
+| `authorize` | ✅ |
+| `checkSubscription` | ✅ |
+| **Player** |
+| `play` | ✅ |
+| `pause` | ✅ |
+| `togglePlayerState` | ✅ |
+| `skipToNextEntry` | ✅ |
+| `getCurrentState` | ✅ |
+| `addListener` | ✅ |
+| `removeListener` | ✅ |
+| **MusicKit** |
+| `catalogSearch` | ✅ |
+| `getTracksFromLibrary` | ✅ |
+| `setPlaybackQueue` | ✅ |
## Installation
```sh
npm install @lomray/react-native-apple-music
```
+- In your Podfile, set minimum IOS target for Pod installation:
+```sh
+platform :ios, 15.0
+```
```sh
npx pod-install
```
@@ -43,6 +49,7 @@ npx pod-install
Allow to continue
```
- Ensure that your Apple Developer account has the MusicKit entitlement enabled and the appropriate MusicKit capabilities are set in your app's bundle identifier.
+- It's highly recommended to set a minimum deployment target of your application to 16.0 and above, because MusicKit requires it to perform a catalog search and some convertation methods.
## Linking
@@ -139,7 +146,7 @@ Search the Apple Music catalog:
```javascript
async function searchCatalog(query) {
try {
- const types = ['songs', 'albums']; // Define the types of items you're searching for
+ const types = ['songs', 'albums']; // Define the types of items you're searching for. The result will contain items among songs/albums
const results = await MusicKit.catalogSearch(query, types);
console.log('Search Results:', results);
} catch (error) {
@@ -148,6 +155,37 @@ async function searchCatalog(query) {
}
```
+
+### Getting user's recently played songs or albums
+Get a list of recently played items:
+
+```javascript
+async function getTracksFromLibrary() {
+ try {
+ const results = await MusicKit.getTracksFromLibrary();
+
+ console.log('User`s library Results:', results);
+ } catch (error) {
+ console.error('Getting user tracks failed:', error);
+ }
+}
+```
+
+
+
+### Set a playback Queue
+Load a system Player with Song, Album, Playlist or Station, using their ID:
+
+```javascript
+async function setPlaybackQueue() {
+ try {
+ await MusicKit.setPlaybackQueue("123456", "album");
+ } catch (error) {
+ console.error('Setting playback queue:', error);
+ }
+}
+```
+
### Using Hooks
The package provides hooks for reactive states in your components:
diff --git a/ios/MusicModule.m b/ios/MusicModule.m
index bcea500..33312d7 100644
--- a/ios/MusicModule.m
+++ b/ios/MusicModule.m
@@ -10,6 +10,10 @@ @interface RCT_EXTERN_MODULE(MusicModule, NSObject)
RCT_EXTERN_METHOD(catalogSearch:(NSString *)term types:(NSArray *)types options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+RCT_EXTERN_METHOD(setPlaybackQueue:(NSString *)itemId type:(NSString *)type resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(getTracksFromLibrary:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+
RCT_EXTERN_METHOD(play)
RCT_EXTERN_METHOD(pause)
RCT_EXTERN_METHOD(skipToNextEntry)
diff --git a/ios/MusicModule.swift b/ios/MusicModule.swift
index 636bfd4..76f6734 100644
--- a/ios/MusicModule.swift
+++ b/ios/MusicModule.swift
@@ -102,6 +102,7 @@ class MusicModule: RCTEventEmitter {
return
}
+
switch currentEntry.item {
case .song(let song):
Task {
@@ -129,7 +130,40 @@ class MusicModule: RCTEventEmitter {
}
case .musicVideo(let musicVideo):
- print("The current item is a music video: \(musicVideo.title)")
+ Task {
+ print("The current item is a music video: \(musicVideo.title)")
+
+ let request = MusicCatalogResourceRequest(matching: \.id, equalTo: musicVideo.id)
+ do {
+ let response = try await request.response()
+ if let foundMusicVideo = response.items.first {
+ if #available(iOS 16.0, *) {
+ let songInfo = self.convertMusicVideosToDictionary(foundMusicVideo)
+ DispatchQueue.main.async {
+ completion(songInfo)
+ }
+ } else {
+ print("Update your IOS version to 16.0>")
+ DispatchQueue.main.async {
+ completion(nil)
+ }
+ }
+ } else {
+ print("Music video not found in the response.")
+ DispatchQueue.main.async {
+ completion(nil)
+ }
+ }
+ } catch {
+ print("Error requesting music video: \(error)")
+ DispatchQueue.main.async {
+ completion(nil)
+ }
+ }
+ }
+
+ case .some(let some):
+ print("The current item is some item:\(some.id)")
completion(nil)
default:
@@ -173,10 +207,12 @@ class MusicModule: RCTEventEmitter {
let canPlayCatalogContent = capabilities.contains(.musicCatalogPlayback)
let hasCloudLibraryEnabled = capabilities.contains(.addToCloudMusicLibrary)
+ let isMusicCatalogSubscriptionEligible = capabilities.contains(.musicCatalogSubscriptionEligible)
let subscriptionDetails: [String: Any] = [
"canPlayCatalogContent": canPlayCatalogContent,
- "hasCloudLibraryEnabled": hasCloudLibraryEnabled
+ "hasCloudLibraryEnabled": hasCloudLibraryEnabled,
+ "isMusicCatalogSubscriptionEligible": isMusicCatalogSubscriptionEligible
]
resolve(subscriptionDetails)
@@ -276,6 +312,77 @@ class MusicModule: RCTEventEmitter {
]
}
+ func convertAlbumToDictionary(_ album: Album) -> [String: Any] {
+ var artworkUrlString: String = ""
+
+ if let artwork = album.artwork {
+ let artworkUrl = artwork.url(width: 200, height: 200)
+
+ if let url = artworkUrl, url.scheme == "musicKit" {
+ print("Artwork URL is a MusicKit URL, may not be directly accessible: \(url)")
+ } else {
+ artworkUrlString = artworkUrl?.absoluteString ?? ""
+ }
+ }
+
+ return [
+ "id": String(describing: album.id),
+ "title": album.title,
+ "artistName": album.artistName,
+ "artworkUrl": artworkUrlString,
+ "trackCount": String(album.trackCount)
+ ]
+ }
+
+ @available(iOS 16.0, *)
+ func convertMusicItemsToDictionary(_ track: RecentlyPlayedMusicItem) -> [String: Any] {
+ var resultCollection: [String: Any] = [
+ "id": String(describing: track.id),
+ "title": track.title,
+ "subtitle": String(describing: track.subtitle ?? "")
+ ]
+
+ switch track {
+ case .album:
+ resultCollection["type"] = "album"
+ break
+ case .playlist:
+ resultCollection["type"] = "playlist"
+ break
+ case .station:
+ resultCollection["type"] = "station"
+ break
+ default:
+ resultCollection["type"] = "unknown"
+ }
+
+ return resultCollection
+ }
+
+ @available(iOS 16.0, *)
+ func convertMusicVideosToDictionary(_ musicVideo: MusicVideo) -> [String: Any] {
+ var artworkUrlString: String = ""
+
+ if let artwork = musicVideo.artwork {
+ let artworkUrl = artwork.url(width: 200, height: 200)
+
+ if let url = artworkUrl, url.scheme == "musicKit" {
+ print("Artwork URL is a MusicKit URL, may not be directly accessible: \(url)")
+ } else {
+ artworkUrlString = artworkUrl?.absoluteString ?? ""
+ }
+ }
+
+ return [
+ "id": String(describing: musicVideo.id),
+ "title": musicVideo.title,
+ "artistName": musicVideo.artistName,
+ "artworkUrl": artworkUrlString,
+ "duration": musicVideo.duration!
+ ]
+ }
+
+
@objc(catalogSearch:types:options:resolver:rejecter:)
func catalogSearch(_ term: String, types: [String], options: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
Task {
@@ -302,10 +409,135 @@ class MusicModule: RCTEventEmitter {
print("Response received: \(response)")
let songs = response.songs.compactMap(convertSongToDictionary)
- resolve(["results": songs])
+ let albums = response.albums.compactMap(convertAlbumToDictionary)
+
+ resolve(["songs": songs, "albums": albums])
} catch {
reject("ERROR", "Failed to perform catalog search: \(error)", error)
}
}
- }
+}
+
+ @available(iOS 16.0, *)
+ @objc(getTracksFromLibrary:rejecter:)
+ func getTracksFromLibrary(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
+ Task {
+ do {
+ let request = MusicRecentlyPlayedContainerRequest()
+ let response = try await request.response()
+
+ let tracks = response.items.compactMap(convertMusicItemsToDictionary)
+
+ resolve(["recentlyPlayedItems": tracks])
+ } catch {
+ reject("ERROR", "Failed to get recently played tracks: \(error)", error)
+ }
+ }
+ }
+
+ @objc(setPlaybackQueue:type:resolver:rejecter:)
+ func setPlaybackQueue(_ itemId: String, type: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
+ Task {
+ do {
+ let musicItemId = MusicItemID.init(itemId)
+
+ if let requestType = MediaType.getRequest(forType: type, musicItemId: musicItemId) {
+ switch requestType {
+ case .song(let request):
+ // Use request for song type
+ let response = try await request.response()
+
+ guard let tracksToBeAdded = response.items.first else { return }
+
+ let player = SystemMusicPlayer.shared
+
+ player.queue = [tracksToBeAdded] /// <- directly add items to the queue
+
+ try await player.prepareToPlay()
+
+ resolve("Track(s) are added to queue")
+
+ return
+
+ case .album(let request):
+ // Use request for album type
+ let response = try await request.response()
+
+ guard let tracksToBeAdded = response.items.first else { return }
+
+ let player = SystemMusicPlayer.shared
+
+ player.queue = [tracksToBeAdded] /// <- directly add items to the queue
+
+ try await player.prepareToPlay()
+
+ resolve("Album is added to queue")
+
+ return
+
+ case .playlist(let request):
+ // Use request for playlist type
+ let response = try await request.response()
+
+ guard let tracksToBeAdded = response.items.first else { return }
+
+ let player = SystemMusicPlayer.shared
+
+ player.queue = [tracksToBeAdded] /// <- directly add items to the queue
+
+ try await player.prepareToPlay()
+
+ resolve("Playlist is added to queue")
+
+ return
+
+ case .station(let request):
+ // Use request for station type
+ let response = try await request.response()
+
+ guard let tracksToBeAdded = response.items.first else { return }
+
+ let player = SystemMusicPlayer.shared
+
+ player.queue = [tracksToBeAdded] /// <- directly add items to the queue
+
+ try await player.prepareToPlay()
+
+ resolve("Station is added to queue")
+
+ return
+
+ }
+ } else {
+ print("Unknown media type.")
+
+ return
+ }
+ } catch {
+ reject("ERROR", "Failed to set tracks to queue: \(error)", error)
+ }
+ }
+ }
+
+ enum MediaType {
+ case song(MusicCatalogResourceRequest)
+ case album(MusicCatalogResourceRequest)
+ case playlist(MusicCatalogResourceRequest)
+ case station(MusicCatalogResourceRequest)
+
+ static func getRequest(forType type: String, musicItemId: MusicItemID) -> MediaType? {
+ switch type {
+ case "song":
+ return .song(MusicCatalogResourceRequest(matching: \.id, equalTo: musicItemId))
+ case "album":
+ return .album(MusicCatalogResourceRequest(matching: \.id, equalTo: musicItemId))
+ case "playlist":
+ return .playlist(MusicCatalogResourceRequest(matching: \.id, equalTo: musicItemId))
+ case "station":
+ return .station(MusicCatalogResourceRequest(matching: \.id, equalTo: musicItemId))
+ default:
+ return nil
+ }
+ }
+ }
}
diff --git a/src/index.ts b/src/index.ts
index 8fbc921..70129ce 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,6 +10,10 @@ export * from './types/auth-status';
export * from './types/check-subscription';
+export * from './types/music-item';
+
+export * from './types/tracks-from-library';
+
import useCurrentSong from './hooks/use-current-song';
import useIsPlaying from './hooks/use-is-playing';
import Auth from './modules/auth';
diff --git a/src/modules/auth.ts b/src/modules/auth.ts
index 407ad83..5883cc1 100644
--- a/src/modules/auth.ts
+++ b/src/modules/auth.ts
@@ -32,11 +32,12 @@ class Auth {
return result;
} catch (error) {
- console.warn('Apple Music Kit: Check subscription failed.', error);
+ console.error('Apple Music Kit: Check subscription failed.', error);
return {
canPlayCatalogContent: false,
hasCloudLibraryEnabled: false,
+ isMusicCatalogSubscriptionEligible: false,
};
}
}
diff --git a/src/modules/music-kit.ts b/src/modules/music-kit.ts
index 6b2b0c0..755e065 100644
--- a/src/modules/music-kit.ts
+++ b/src/modules/music-kit.ts
@@ -1,6 +1,8 @@
import { NativeModules } from 'react-native';
import type { CatalogSearchType } from '../types/catalog-search-type';
+import type { MusicItem } from '../types/music-item';
import type { ISong } from '../types/song';
+import type { ITracksFromLibrary } from '../types/tracks-from-library';
const { MusicModule } = NativeModules;
@@ -13,7 +15,7 @@ class MusicKit {
/**
* Searches the Apple Music catalog using the specified search terms, types, and options.
* @param {string} search - The search query string.
- * @param {ICatalogSearchType[]} types - The types of catalog items to search for.
+ * @param {CatalogSearchType[]} types - The types of catalog items to search for.
* @param {IEndlessListOptions} [options] - Additional options for the search.
* @returns {Promise} A promise that resolves to the search results.
*/
@@ -30,6 +32,37 @@ class MusicKit {
return [];
}
+
+ /**
+ * @param itemId - ID of collection to be set in a player's queue
+ * @param {MusicItem} type - Type of collection to be found and set
+ * @returns {Promise} A promise is resolved when tracks successfully added to a queue
+ */
+ public static async setPlaybackQueue(itemId: string, type: MusicItem): Promise {
+ try {
+ await MusicModule.setPlaybackQueue(itemId, type);
+ } catch (error) {
+ console.error('Apple Music Kit: Setting Playback Failed.', error);
+ }
+ }
+
+ /**
+ * Get a list of recently played items in user's library
+ * @return {Promise} A promise returns a list of recently played items including tracks, playlists, stations, albums
+ */
+ public static async getTracksFromLibrary(): Promise {
+ try {
+ const result = await MusicModule.getTracksFromLibrary();
+
+ return result as ITracksFromLibrary;
+ } catch (error) {
+ console.error('Apple Music Kit: Getting tracks from user library failed.', error);
+
+ return {
+ recentlyPlayedItems: [],
+ };
+ }
+ }
}
export default MusicKit;
diff --git a/src/types/check-subscription.ts b/src/types/check-subscription.ts
index a494278..6dd6983 100644
--- a/src/types/check-subscription.ts
+++ b/src/types/check-subscription.ts
@@ -1,4 +1,14 @@
export interface ICheckSubscription {
+ /**
+ * The device allows playback of Apple Music catalog tracks
+ */
canPlayCatalogContent: boolean;
+ /**
+ * The device allows tracks to be added in user's library
+ */
hasCloudLibraryEnabled: boolean;
+ /**
+ * The device allows subscription to the Apple Music Catalog
+ */
+ isMusicCatalogSubscriptionEligible: boolean;
}
diff --git a/src/types/music-item.ts b/src/types/music-item.ts
new file mode 100644
index 0000000..73ed311
--- /dev/null
+++ b/src/types/music-item.ts
@@ -0,0 +1,6 @@
+export enum MusicItem {
+ SONG = 'song',
+ ALBUM = 'album',
+ PLAYLIST = 'playlist',
+ STATION = 'station',
+}
diff --git a/src/types/tracks-from-library.ts b/src/types/tracks-from-library.ts
new file mode 100644
index 0000000..4261ae5
--- /dev/null
+++ b/src/types/tracks-from-library.ts
@@ -0,0 +1,12 @@
+import type { MusicItem } from './music-item';
+
+export interface IUserTrack {
+ id: number;
+ title: string;
+ subtitle: string;
+ type: MusicItem;
+}
+
+export interface ITracksFromLibrary {
+ recentlyPlayedItems: IUserTrack[];
+}