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 resolver function for a playlist's tracks
The Track
object
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:
- 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 anint
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 thisuri
. - We'll make all of these fields 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 GraphQL 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:
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 field names in your schema, try to be descriptive but concise. For example, we could have chosen duration
as one of the field names. However, duration_ms
(which, in turn, becomes durationMs
in the generated GraphQL schema) is a bit more descriptive on the format of the field 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 field called durationString
. Using GraphQL 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
argument 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
field to Playlist
.
from .track import Trackclass Playlist:# ... other Playlist fieldstracks: list[Track] = strawberry.field(description="The playlist's tracks.")
Let's think about this particular resolver function. So far, the Playlist
class contains simple property resolvers (for id
, name
and description
). Remember, behind the scenes, Strawberry adds default resolvers to properties.
Can we do the same for our tracks
resolver? 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
resolver function, which fetches the data using the get_playlist.asyncio
function.
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 GraphQL schema 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
resolver.
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 Nonereturn 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.
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 fields we need, using the properties from item
.
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 query for a playlist's tracks now! Make sure our server is running with the latest changes.
query GetPlaylistDetails($playlistId: ID!) {playlist(id: $playlistId) {idnamedescriptiontracks {idnamedurationMsexplicituri}}}
With the Variables section set to:
{"playlistId": "6Fl8d6KF0O4V5kFdbzalfW"}
Wow, we've got so many tracks for this playlist!
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
field. Let's try it out.
query GetFeaturedPlaylists {featuredPlaylists {idnamedescriptiontracks {idnameexplicituri}}}
When we run this query, we get an errors
array back with the message "Playlist.__init__() missing 1 required keyword-only argument: 'tracks'"
. Uh-oh!
Key takeaways
- Resolver functions may involve navigating through multiple levels of data, especially in scenarios with nested objects.
- We recommend choosing descriptive yet concise field names for the schema.
Up next
Let's investigate the source of that error and fix it.
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.