12. A playlist's tracks
5m

Overview

We're still missing something: a playlist's tracks — the songs we actually want to listen to!

In this lesson, we will:

  • Create the Track type
  • Write the function for a playlist's tracks

The Track object

Mockup design of a playlist's tracks

Take a few moments to study the mockup above and start thinking of what pieces of data we'll need, and what types they might be. We'll ignore the artist information for now to keep things simpler.

You might also want to check out the GET /playlists/{playlist_id}/tracks endpoint for a better idea of what the API returns and what they've named certain properties.

Take your time!

When you're ready, compare it with what we came up with:

Mockup design of a playlist's tracks

  • The track's name is a str.
  • The "E" label denotes if the track is explicit. We can make that a bool type and let the client use the logic to display an "E" label or not.
  • The duration of the track. The mockup shows the formatting to be in minutes and seconds with a colon in between, so maybe we might need to make this a str type, or maybe the client team wants to control their own formatting and we should return it as an int type with the duration in milliseconds. The REST endpoint returns the latter, so let's stick with that for now.
  • Though it's not shown on the mockup, it's helpful to have an identifier for an object, so we'll make sure to return the track's ID as well.
  • There's an option to copy a link to the track to share with others so they can open it on Spotify as well. So we'll probably need to return that link as a str. In the REST API, they've named this uri.
  • We'll make all of these non-nullable since the REST API does the same.

Your design might have looked a little different from ours, but with these pieces in mind, let's go ahead and create our Track class! Remember, we're keeping all our types organized together under the api/types folder.

You should have everything you need to know to try writing this one out yourself! If you need a reference, feel free to use the one below:

api/types/track.py
import strawberry
@strawberry.type(description="A single audio file, usually a song.")
class Track:
id: strawberry.ID = strawberry.field(description="The ID for the track.")
name: str = strawberry.field(description="The name of the track.")
duration_ms: int = strawberry.field(description="The track length in milliseconds.")
explicit: bool = strawberry.field(description="Whether or not the track has explicit lyrics (true = yes it does; false = no it does not OR unknown)")
uri: str = strawberry.field(description="The URI for the track, usually a Spotify link.")

Tip: When choosing names in your schema, try to be descriptive but concise. For example, we could have chosen duration as one of the names. However, duration_ms (which, in turn, becomes durationMs in the generated ) is a bit more descriptive on the format of the returning in milliseconds. We could have also chosen durationInMilliseconds for further clarity. If we wanted to also support returning a formatted string, we could add a new called durationString. Using descriptions also helps with clarity. Learn more about schema naming conventions in the Apollo documentation.

Don't forget to add descriptions to the schema using the description on strawberry.type and strawberry.field.

Connecting tracks to a playlist

With our Track class all set up, we can now add the long awaited tracks to Playlist.

api/types/playlist.py
from .track import Track
class Playlist:
# ... other Playlist fields
tracks: list[Track] = strawberry.field(description="The playlist's tracks.")

Let's think about this particular function. So far, the Playlist class contains simple property (for id, name and description). Remember, behind the scenes, Strawberry adds default to properties.

Can we do the same for our tracks ? Well, let's examine where our data is coming from. For this next section, we highly recommend using your code editor's features to Cmd/Ctrl + click on a particular type to navigate to its type navigation!

The details for a particular playlist are coming from the Query.playlist function, which fetches the data using the get_playlist.asyncio function.

api/query.py
async def playlist(id: strawberry.ID, info: strawberry.Info) -> Playlist | None:
client = info.context["spotify_client"]
data = await get_playlist.asyncio(client=client, playlist_id=id)

If we check the return type for the get_playlist.asyncio function we see that it returns a SpotifyObjectPlaylist. This type has a property tracks of type SpotifyObjectPaginatedSpotifyObjectPlaylistTrack. SpotifyObjectPaginatedSpotifyObjectPlaylistTrack then has a number of other properties related to pagination, and a property called items, which is a list of SpotifyObjectPlaylistTrack types.

Following that type, we get more properties related to the playlist track's metadata, like who added it and when, as well as a property called Track, which is of type SpotifyObjectPlaylistTrackItem. Finally, this looks like the track information we're looking for! Among other properties, it has id, name, explicit, duration_ms properties, which match exactly what our Track class in our has.

Whew! All that to say, we have to dig a couple levels deeper to return the values we need.

However, we've actually done the first level already. We've turned a SpotifyObjectPlaylist type into our own Playlist class in the Query.playlist .

api/query.py
async def playlist(id: strawberry.ID, info: strawberry.Info) -> Playlist | None:
client = info.context["spotify_client"]
data = await get_playlist.asyncio(client=client, playlist_id=id)
if data is None:
return None
return Playlist(
id=strawberry.ID(data.id),
name=data.name,
description=data.description,
)

Now we can update the returned object. We'll add the new property to the instantiation, tracks, which will return a list.

api/query.py
return Playlist(
id=strawberry.ID(data.id),
name=data.name,
description=data.description,
tracks=[
...
],
)

Inside the list, we'll map over each item in data.tracks.items.

Then, create an instance of the Track class with all the we need, using the properties from item.

api/query.py
tracks=[
Track(
id=strawberry.ID(item.track.id),
name=item.track.name,
duration_ms=item.track.duration_ms,
explicit=item.track.explicit,
uri=item.track.uri,
)
for item in data.tracks.items
],

We'll also need to import the Track type at the top of our file:

from .types.track import Track

Explorer time!

That was a long journey, but we should have enough to for a playlist's tracks now! Make sure our server is running with the latest changes.

GraphQL operation
query GetPlaylistDetails($playlistId: ID!) {
playlist(id: $playlistId) {
id
name
description
tracks {
id
name
durationMs
explicit
uri
}
}
}

With the Variables section set to:

Variables
{
"playlistId": "6Fl8d6KF0O4V5kFdbzalfW"
}
http://localhost:8000

Explorer - get playlist's tracks

Wow, we've got so many tracks for this playlist!

Task!

An alternate path

Now what about the featuredPlaylists path? It's another entry point to our schema that returns a list of Playlist types, which then has access to its tracks . Let's try it out.

GraphQL operation
query GetFeaturedPlaylists {
featuredPlaylists {
id
name
description
tracks {
id
name
explicit
uri
}
}
}

When we run this , we get an errors array back with the message "Playlist.__init__() missing 1 required keyword-only argument: 'tracks'". Uh-oh!

Key takeaways

  • functions may involve navigating through multiple levels of data, especially in scenarios with nested objects.
  • We recommend choosing descriptive yet concise names for the schema.

Up next

Let's investigate the source of that error and fix it.

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.