My confusion stemmed from the fact that the most played playlists are added in the results even though the type is only Song.self.
If I hit the endpoint:
https://api.music.apple.com/v1/catalog/us/charts?types=songs&chart=most-played&with=cityCharts
The corresponding result only has the most played songs and city top charts:
"results": {
"order": [
"most-played:songs",
"city-top:cityCharts"
]
}
But MusicCatalogChartsRequest also adds the most played playlists
Any update on this? It is still returning nil in Xcode 13.4
Post not yet marked as solved
While Joe will be able to provide a perfect answer, you can use the following structure to decode the relationship:
struct RecentlyAdded: Decodable {
var data: [RecentlyAddedData]
struct RecentlyAddedData: Decodable {
var relationships: Relationships
struct Relationships: Decodable {
var catalog: MusicItemCollection<Album>
}
}
}
I am not sure how you can leverage Album and associate the relationship directly with it. Also, if you don't know, the recently-added endpoint provides playlists and stations resources apart from albums.
Looking forward to Joe's answer.
Hi @JoeKun, with the current solution and the response from the endpoint, it only fetches one item at a time. How would I go about creating a MusicItemCollection out of it? The Search for Catalog Resources has a very nice response as it provides an array of albums, songs, etc.
But the Get Catalog Search Suggestions has one response model per music item, be it an album, song. Etc.
Any suggestion?
Edit: Hacky workaround
switch topResult.content {
case .album(let album): self.albums += MusicItemCollection(arrayLiteral: album)
case .artist(let artist): self.artists += MusicItemCollection(arrayLiteral: artist)
case .song(let song): self.songs += MusicItemCollection(arrayLiteral: song)
case .curator(let curator): self.curators += MusicItemCollection(arrayLiteral: curator)
default: ()
}
Do you think I should use MusicItemCollection in this way?
Post not yet marked as solved
You can get the "Favourite Mix" and "New Music Mix" if you hit the recommendations API that returns a collection of playlists. Have you tried that?
https://api.music.apple.com/v1/me/recommendations
Oh yes, if you see my code example, Genres is a typealias for MusicItemCollection<Genre>! :D
MusicItemCollection is one of the best structures in MusicKit, and it simplifies so many things.
Try wrapping the view inside ForEach with a VStack:
List {
ForEach(intervals, id: \.id) { interval in
Section {
VStack { // <-- HERE
Text(interval.name)
}
.id(interval.id)
}
}
}
Post not yet marked as solved
I faced the same problem and wrote an article as a workaround!
https://rudrank.blog/musickit-for-swift-artist-artwork
Post not yet marked as solved
Edit: I didn't see that this question was for MusicKit JS. Apologies.
I don't know how to delete the answer, so I'll leave it for someone else to find it helpful.
You can extract the song ID from the link by creating a URL component and getting the query items out of the component.
Here, the query parameter is "i" with the value 1035048414 as the song ID. You get the first query item and fetch the value out of it:
let components = URLComponents(string: "https://music.apple.com/us/album/take-on-me-1985-12-mix-2015-remastered/1035047659?i=1035048414")
guard let songID = components?.queryItems?.first?.value else { return }
print("SONG ID IS - \(songID)")
Now, you can just use a standard MusicCatalogResourceRequest:
let request = MusicCatalogResourceRequest<Song>(matching: \.id, equalTo: MusicItemID(songID))
do {
let response = try await request.response()
print(response.description)
} catch {
print(error)
}
When I print it:
SONG ID IS - 1035048414
MusicCatalogResourceResponse<Song>(
items: [
Song(id: "1035048414", title: "Take On Me (1985 12" Mix) [2015 Remastered]", artistName: "a-ha")
]
)
I hope that helps!
Post not yet marked as solved
Hi @JoeKun,
Apologies for the delay. I've filed feedback - FB9647701
Thanks!
What should be the best approach to decode the chart data for songs?
For example -
struct Charts: Decodable {
let results: Songs
}
struct Songs: Codable {
let songs: [ChartsSong]
}
struct ChartsSong: Codable {
let data: [Song]
let orderID, next, chart, name: String
let href: String
enum CodingKeys: String, CodingKey {
case data
case orderID = "orderId"
case next, chart, name, href
}
}
let url = URL(string: "https://api.music.apple.com/v1/catalog/us/charts?types=songs&genre=20&limit=1")!
let musicRequest = MusicDataRequest(urlRequest: URLRequest(url: url))
let response = try await musicRequest.response()
let data = try JSONDecoder().decode(Charts.self, from: response.data)
Right now, I'm using a custom structure to decode the data with the ChartsSong struct. Do you think there's a possibility of a Chart item in the future? Should I file feedback stating my use case?
Hi @JoeKun,
While creating my own framework for easy access to different Apple Music APIs, I realized I was using it wrong. It's better to decode using MusicItemCollection<> instead of creating my own structure. Then, it safely ignores the item that is not decodable due to missing information.
let response = try JSONDecoder().decode(MusicItemCollection<Song>.self, from: dataResponse.data)
Hi @joekun,
This is the custom struct -
struct Songs: Decodable {
let data: [Song]
}
where Song is the new structure in MusicKit for Swift.
Hi @JoeKun,
Thank you for your detailed answer! Much appreciated.
So, if I've to show the current duration in a label, I should probably have a timer running with the initial value of playbackTime? And then, whenever the playbackStatus changes, update the current duration value with the latest playbackTime and refresh the view?
I think I can do with this approach for the time being. But having playbackTime as an observable value will make it much much simpler. So, I'll file a ticket for this.
Thank you so much!
Post not yet marked as solved
Hi @RanLearns!
I was in the same position as you when Apple released beta 4. I was able to fix the issues mainly around the play() being asynchronous now and can throw an error. I don't know if my approach is the standard, one but I'll share it nonetheless.
Dividing the error into two parts -
Trying to make an async call from a non-async function.
The call can throw errors, but the function is not handling the error.
For the first part, we can suffix the function with the async keyword and prefix the call with the await keyword.
For example, `handlePlayButtonSelected() becomes -
private func handlePlayButtonSelected() async {
if !isPlaying {
if !isPlaybackQueueSet {
player.setQueue(with: album)
isPlaybackQueueSet = true
}
await player.play()
} else {
player.pause()
}
}
We can make the function throw an error and prefix the call with the try keyword for the second part. So, handlePlayButtonSelected() finally looks like -
private func handlePlayButtonSelected() async throws {
if !isPlaying {
if !isPlaybackQueueSet {
player.setQueue(with: album)
isPlaybackQueueSet = true
}
try await player.play()
} else {
player.pause()
}
}
Similarly, `handlePlayButtonSelected() can be changed to -
private func handleTrackSelected(_ track: Track, loadedTracks: MusicItemCollection<Track>) async throws {
player.setQueue(with: loadedTracks, startingAt: track)
isPlaybackQueueSet = true
try await player.play()
}
Now, you'll get another error in view where you call these functions. First, in the list where you show the TrackCell, update the action handleTrackSelected(track, loaded tracks: loadedTracks) with the following -
Task {
try? await handleTrackSelected(track, loadedTracks: loadedTracks)
}
For the second error in the button in playButtonRow, update handlePlayButtonSelected with -
Task {
try? await handlePlayButtonSelected()
}
The third error is due to a change in the searchable modifier. Now, you've to specify a prompt.
So, replace searchable("Albums", text: $searchTerm) with -
.searchable(text: $searchTerm, prompt: "Albums")
There are a few warnings as well, mostly related to the use of detach. You can replace them with Task.detached.
Lastly, you'll find deprecation warnings related to the setQueue(with:startingAt:) method. In Xcode 13, Beta 4 added a new instance property, queue.
We can set this property by using the initializers of the class Queue. In AlbumDetailView.handleTrackSelected(_:loadedTracks:), you can set it as -
private func handleTrackSelected(_ track: Track, loadedTracks: MusicItemCollection<Track>) async throws {
player.queue = .init(for: loadedTracks, startingAt: track)
isPlaybackQueueSet = true
try await player.play()
}
In handlePlayButtonSelected(), you can set it as -
private func handlePlayButtonSelected() async throws {
if !isPlaying {
if !isPlaybackQueueSet {
player.queue = .init(arrayLiteral: album)
isPlaybackQueueSet = true
}
try await player.play()
} else {
player.pause()
}
}
The last warning is related to the deprecation of the playbackStatus variable. The latest beta offers us with ObservableObject class MusicPlayer.State, and we'll use the playbackState instance property instead. I'm not sure about this one on how to go about observing the value of the playbackStatus, but here's my approach -
.task {
isPlaying = player.state.playbackStatus == .playing
}
With this, the project is error and warnings-free!