Parsing & Importing XML: Adding Podcast Episodes, and Playlists
Building a GRANDstack Podcast App: Episode 3
In Episode 3 we explore how to parse and import XML data into Neo4j as we add podcast episodes and playlists to our GraphQL API.
Links And Resources#
- Code on Github
- GRANDstack docs: Cypher parameters from context
- APOC User Guide
- Cypher refcard
- RSS 2.0 Specifcation
Hey folks welcome to the neo4j stream. My name is: will uh and today we are continuing work on the grand cast fm podcast application built with grand stack we've been working on um. I guess this is the third session uh. Let me link the code to start off if you haven't seen that so there's the code, it's this github page that i have on the screen here. You can find the previous recordings linked here, episodes one and two in episode. One we got started building out uh the podcast search functionality is what we focused on in episode, one using a service called the podcast index and we built a graphql api to allow users to search for podcasts. Then, in episode 2 we introduced the concept of users and subscribing to podcasts and we sort of rolled our own authentication system using passwords and json web token. So now we have a graphql api, any user can create an account they can log in get an authorization. Token subscribe to podcasts and then any user, whether you're, authenticated or not, can search for podcasts, and what we want to do today is okay. You can search for a podcast, you can subscribe to it, but we don't yet have the concept of episodes. So what we're going to do today is figure out how to parse these podcast feeds. Podcast feeds are our xml documents that conform to the rss spec, so we're going to figure out how to parse those and import those episodes into the database. And then we want users to be able to um, create playlists and add episodes to their playlists. So once we've parsed a podcast feed, i want to be able to see all the episodes for it as a user. I want to be able to add individual episodes to my playlists. I want to view the episodes of a playlist and i want to view my feed. I guess so. I want to view in chronological order the latest episodes that i subscribe to uh cool. So that's what we're gon na focus on today, uh! So picking up from where we left off last time, working with user authentication, uh, let's fire up our api, so we'll say, start dev that will create our graphql api running on the localhost 4001. Let's make that a bit bigger, so i have my authorization token. From last time, so that looks good. I have that here um, so we don't need to go through the login process, again uh, although we could, although i may have forgotten my password and then if we look in the docs tab, got the spinner not sure. Why give that a sec make sure we have our database yep, so i've got the database running locally in neo4j desktop good. So, while chrome is waking up here, let's talk a little bit about how we're going to approach this problem of parsing xml feeds, uh and and podcast feeds. So there's a few ways that we could go about this um. We could sort of write some some code as a worker, maybe like some python code or some node code and have a process that is checking the database for what are the podcasts that exist in the database and then going out to the podcast url parsing. That xml and then calling the database to load it in the database, so that would be one one way to do it um. We then have to run these workers somewhere, as maybe lambda functions, that get triggered every five minutes or whatever, or we could have a server running somewhere. That'S just running these workers continuously, so that would be one option um. What we're going to do, though, today is actually parse. These podcast feeds using cipher, so we're going to use the database actually to parse these and import data into the database and specifically we're going to use apoc again. We used apoc previous oops, not your dot com. Apoc i wanted to go to slash labs and then go to the apox apoc documentation, so we used apoc previously in our graphql schema here for podcast search. So we used apoc to call out to the podcast index api and apoc allowed us to make that network request to the podcast index. Api set, our authentication headers uh read our credentials from a config file, so apoc is really handy. It'S this library that extends the functionality of cipher quite a bit and if we look in apoc import load, xml is one of our options. So we're going to use the apoc load xml procedure to import xml data, specifically our podcast feed, and insert that data in the database. We could use the the podcast index. So if we look at the docs podcast index, if you look at the docs for the podcast index, there is a uh episodes in point, so we could uh fetch episodes from the podcast index um as well. I i want to see how difficult it is to parse these podcast feeds ourselves, though um i'm sure we're going to run into some some feeds that are in different format, or maybe some that don't conform to the specification and - and things like that, so we'll have To work our way around that but um, let's give it a try to see if we can can parse these uh ourselves. Okay. So if we look at the the documentation here looks like we have the url to our xml documents. We call apoc load xml zoom in a little bit here, apoc load xml, passing in some configuration. I have to see what configuration objects. Look like simple mode, so it looks like there's simple mode: let's go back to apoc load xml, so in simple mode, each type of children has its own entry in the parent map. Okay, so that's what simple mode means and the element type is prefixed with an underscore okay cool. So, basically, what this procedure does is it takes xml and creates a map data structure so like like a dictionary or an object and then returns that object, that map for us to then work with in cypher, and it uses these conventions to translate that xml document Into a map - and so the configuration we can say, although we want to fail on an error while parsing the document and whether or not we want to set any headers, so headers uh in this case would be. If we have some, maybe authentication headers. We needed to add to fetch this, but in this case most of our podcast feeds should hopefully be be open and publicly accessible cool. So we shouldn't need to worry about that. If we look down at the example here, so here's a sample xml document, we have a catalog, we have books and then each book has an author title, genre, price, publish and description tag and here's an example of how the map looks when it is created. So you can see that that hierarchy is preserved. The type of the node in the xml document gets an underscore underscore type, is the key and the value and then underscore children to preserve that hierarchy and then that's just sort of nested cool. So that is roughly how it works. Let'S go into neo4j and first, let's see, i think we subscribed to some podcasts last time. Yep got a couple podcasts, let's make sure our graphql api is, is up and running here. So we last time added the ability to sign in to to search for podcasts to show who the current user is. So, if i say current user give me my username, this will use the authentication token that i've passed it will validate. It look up that user in the database after it validates that the token is properly signed and give us the username cool. So i'm logged in as jennycat uh. Let'S see, let's see what podcasts oops jennycat has subscribed to so title itunes id, so we've subscribed to graphistania and the connected podcast and each one of these has a feed url. So if we go to this feed url, let's copy this and look at this in the browser drop a link to this in the chat. If you want to see what this, what this looks like zoom in a bit okay, so here's what this xml document looks like this is the feed for the graphistania podcast. So we have a channel tag. Then we have some metadata about the channel. So that's information about the podcast. Then we have uh episode 11 here which starts with an item. So we have an item tag and then the item has things like a good. So this represents the podcast episode, so it has a unique id, a title, a pub date, a link, some itunes, specific tags, a description, and then this is an important tag. Called an enclosure and the enclosure has the type. So this is like the content type of the link which in this case is the mp3 file and then the length. I think this is the size of the file in bytes, and then we have the end item tag and then on to the next one, which represents the next episode. So that is basically what makes a podcast there's. Let'S see the rss2 specifications, what i wanted to look at there's a specification that every podcast feed needs to follow, and that is the rss to specification. So a podcast is basically any xml document that anyone can put out there that conforms to the rss2 specification that links the audio files in in that enclosure link. So the neat thing about podcasts is that anyone can publish these just create your own podcast feed and start putting content out. Rss stands for the really simple syndication and if we look at the specification, we'll see we'll see the things here that the graphistania podcast conform to. So the channel element is required to have title link description, there's a bunch of other optional things uh, but it needs to have at least those required elements. So like you saw that it has um additional elements that are specific to itunes. So the itunes category, which is going to be not necessarily included on all of these, so we want to make sure to handle that anyway. Then the item element - this is an important one as well. It has title link description, enclosure, good pub date. We saw those cool, so that is what we're interested in. Let'S take this feed url and jump into neo4j and see if we can figure out how to start parsing this using cipher. So we said we wanted to use apoc load xml. This will yield value and value is going to be. This map object, let's go ahead and just return that and see what that looks like so now. This is the object. We'Re going to be working with, so you can see. We have a top level type that type has children of type channel. The channel will then have children as well, which are going to be like each of the episodes which are going to be of of type item, and then the channel also has metadata associated with it. So, let's start pulling out some of the values that we're interested in um [, Music ]. We don't need. We already have the the podcast information uh, so the metadata associated with the podcast itself. So, like the title description, the feed url the sort of stuff - we already have that from our call to the podcast index. Api we've already saved that in the database. So we don't need to grab that. So, let's just let's just start iterating over the the um children which are going to be the child elements of this xml document. We don't really need to to name these, so let's say we want to bring through. We want to filter out. I guess where the type is item, so i want to filter through the children of this foo element where the type is item and those are going to be our episodes. So, let's return those okay cool. So now we have a array of episodes. This looks good, so we see we have information that we need for each episode. So now, let's unwind over each episodes and then let's pull out some of the data that we need here. So, let's filter episode, dot children, let's filter where the type is title so where the child element of item is title. So that's going to be this object. Let'S get the text as title and then we're going to repeat this pattern: the children where the type is, what else do we have summary? And now this um this extract? Let'S look at the cipher ref card and i'll drop a link to the cipher ref card. In the chat here, that's a handy resource to see all the different syntax for cipher, so that list expression that we're using is uh doing a filter where the predicate is true right. So i want to filter on this object, and then it gives me back a list in this children array right. The reason i have to grab the first one is because there could be more than one object in this children list that match this predicate there's most likely not going to be right, because, if we're conforming to the rss specification, we should just have one of these Um, anyway, that's why we're doing that? So this is going to be the summary uh filter children, where the type is, i don't know, x, dot, underscore type. Uh summary we have the link and we want the text next. In episode, children, where x dot type is what else do we have that we want image? That'S going to be our our show, art x in episode, children, where x, dot underscore type equals uh the enclosure, that's an important one, so the enclosure closure. This is going to be the fog, the mp3 file. We also want to filter for the type of the enclosure. I think you can. You can include kind of arbitrary enclosure urls, so let's also filter [, Music ], where the type let's say contains audio. So it's like the content type of the file. It could be audio impact. I don't know what the other formats are, but could be audio something else. Maybe so, let's do that grab the first elements. Dot url is what we want this time. Instead of text underscore text is like for the simple tags um. If we look at what this enclosure the enclosure itself is a here, we go so the enclosure. Um is a self closing tag right. The enclosure doesn't have any text values like where description has the text value here. It just has um. I don't know what the proper term for this is, but like the metadata associated uh, with the tag, type url and so on. So that's why we're grabbing the url value instead of the text value, because the enclosure tag itself doesn't have that text. Value. We'Ll say this as audio okay x in episode, dot, children where x, dot type equals. Okay, we got the enclosure, we got the image, we got the summary duration. I don't think we need oh yeah. Let'S do pub date, update first element dot text as pub date. So the date the episode was published and episode. Children, where the type is, what else do we need um? We could definitely make use of the unique id the good. So let's grab that okay and let's return star to see if this works sort of. So we have episode title summary: the pub date uh the link, so the difference between link and the audio file. The link is like the landing page for the website or for the for the episode. So in this case, oh it's very loud. It startled me in this case the link is uh hosted on soundcloud, okay, but good and image didn't come through. So what did we do wrong there? It will go good. I miss typed as guide, so here's the unique id for the image um. Oh image is not stored in text it's stored in the href, so instead of text it's stored, sort of inline in that tag as href cool there we go now we have the image file for each episode and the audio file. Let'S make sure that okay well in the browser that tries to download it okay, that's fine, we'll have to figure out how to play those when we start building the the front end of our application, but okay cool. So we've got uh the basic information. Now we need for each episode, um. I guess let's, let's test this on - maybe another podcast. Let'S try this out on connected. This should work, because these should both conform to the same rss2 specification. So we've got audio file, unique id, it's the url, that's fine! I'Ve got image art for the episode link pub date. Summary title cool looks good, so what we want to do now um is actually add these to the database um, let's go ahead and and do that so in this case uh, let's match: let's just go ahead and do this for all podcasts that we have in The database we only have two podcasts in the database, so this should be fine which we're storing, i think it's, the feed, url property is where we're storing the feed url for each property so match on all the podcast nodes, then for each podcast node, which is Just going to be two, i don't know, maybe three that we have in the database so far. Remember the only reason we we have just a couple of data. A couple of podcasts in the database is because we're only si creating a podcast node in the database when a user subscribes to it. So because we're doing search through the podcast index, we don't need to store like every single podcast in existence. We only need to store the ones that our users are subscribing to in the database and that helps us as we're building out the sort of podcast aggregator where we're parsing all of these podcast feeds. But we only need to parse the feeds that our users have subscribed to. There'S no point in us: parsing other podcast feeds okay. So now, let's create an episode episode node for each one of these we'll merge to avoid creating duplicates. Since we have the unique id for each episode, we'll set the episode title, the summary: the link, the image uh audio file - i guess call it audio the pub date and we already set the good. So that's good and then we'll say that the episode has an incoming relationship called. Has episode connected to the podcast up here. Oh, let's make sure we bring so anytime. We have a with statement. We need to explicitly bring through [ Music ], any variable that we've defined that we want to reference later. So, let's see if this works uh invalid input, what did we do wrong? There? Merge episode has episode um, oh extra, comma. There we go. It says: we've added 454 labels. Let'S find some episode nodes cool, so here's an episode node. This is an episode of the connected podcast and we're storing cool all the properties um, let's deal with this pub date, so this pub date. This is now just a string, but we want to store this as a date time type in the database so that we can do like ordering and filtering by it. So we need to cast this during our import to a date. Time object. If we look at, let's see neo4j date time type, so just looking at this for another project, temporal values, instant types - the developer guide on this - i guess - is a good place to start. So we can, let's make this a bit bigger. There we go. We can call the date time or the date function, to create either a date object which has your month day or a date time, which is your month day as well as hour minute. Second millisecond time zone, there's also there's also a zoned or a local date. Time which does not include the uh, does not include the time zone uh and the docs describe how we can create these. I think the docs refers to yeah temporal instant type creation so, depending on the type, there are a few different ways that we can create. These we're going to be working with date time, so we don't want the current. We want to create date time from an existing uh, an existing string, which we can do in a few different formats. However, the format that we have, which is we look at the specification, where is pub date pub date, conforms to the date and time specification of rfc 822. If you look at that, oh it's, a bad link rfc822 anyway. This is the this is an example of rfc 822 format, which is what all of our pub dates should be in. So what we're going to do is actually use another apoc function. I think this one is apoc date. Parse, i think, is what we're after yeah. Here we go uh where epoch date, parse takes a string, the unit that we want to parse it to so we're going to parse this string and create this the equivalent millisecond since the epic format, and then it takes a format string uh, which is used to Define the format of the string that we're reading so there's lots of different formats. So we need to figure out what the uh, what the format is for the rfc. What was it 8, 22, so rfc, 822 java date format and if we take a look at stack, overflow um here is an example that has the date format that we're looking for. So let's try this. So if we say, let's return, apoc date, parse the string, we want to parse two milliseconds and then here's the format that we grabbed. Let'S grab this pub date example, and we should get back this date in milliseconds cool, which we do and so now to convert this to a date. Time object. We can call the datetime function uh and pass in epic millies. I think it is as an object and we should now have a date time type missing a closing parenthesis. It looks like cool, so this is november. 4Th 2020. 11. 30 p.m, gmt great! So let's copy that - and where are we here so now when we save the pub date, we're going to say, call apoc date, parse on the pub date. If we run this again because i'm saying set instead of like on create set where we would only update those values, if we were creating a new episode node but because i say set so when we, when we use merge, there's two options: there's oncreate set, which Will only then update these values in the case where this episode node did not exist and that's useful if these values are not changing or if maybe it's expensive to compute these values, we don't want to do it every time, especially if we're loading large data sets. We want to use oncreateset often, so we don't spend a lot of time, updating, uh the property store with these values, and then we have another control flow which is on match set, which this is useful. If maybe we want to like increment a counter or something like that where, if we've seen this before, we see it again and then, if we just say set then in either case, if it's creating or matching on this episode, node we'll update these values and i'm Going to leave this as set because these uh podcast feeds can update right, so people can push up new podcast files. If there's a mistake, they can update the you know the show notes things like that, so we'll make sure to always overwrite any existing values that we had. So we ran that and now, if we let's go back and look at some episodes, if we look now at an episode, we should see cool now. This is a date time type instead of a string, okay cool, so we parsed all these episodes. That'S that's nice! Uh, how do we keep up with this right? So so our users are going to be searching and subscribing to new podcasts. We want to make sure that we're also checking these feeds to see if there are new podcast episodes being released for the ones that our users are subscribing to. So one thing we can do is run this as a background job. So if we take a look at apoc uh, what section is this in background operations and background jobs? So we can run this statement as a background job. So every i don't know maybe five minutes it might make sense to uh parse our podcast feeds. Of course, we need to be a bit smart about how we do this uh. We we don't want to just try to do this, all in one big cipher statement of something. If one of the feeds fails to parse, it can't fetch it uh for whatever reason uh. Maybe it's doesn't actually conform to the specification. We want to make sure that we have some air handling there uh. So maybe we just have something where we store. The last time that a podcast feed was parsed and we're just grabbing the most recent one uh and continuously checking for new episodes. That kind of thing, so that's that's one option that we can have these running in the background and and managed as an apoc background job. But what about for new podcasts? So when a user first subscribes to a podcast, they want to like see those episodes right away right so anytime that a new podcast is added to the database. We want to parse its feed and add all of its episodes and there's a couple ways to do that. One would be using a trigger, so we could use an apoc trigger so that anytime, a podcast node is created. Then we parse the feed and add any episodes for it, and we could do that by defining an apoc trigger i'll link, the page for this in the chat. So that would be one option. I think another option, though, if we look now at the code for our graphql api, another option would be to do this in the mutation when we subscribe to a podcast so kind of similarly to how, when a user subscribes to a podcast, we're calling out to [ Music ] the podcast index to fetch the details for that podcast. I think right here. What we'll do is parse the podcast feed url and add any episodes during this mutation, so that that way they are available immediately for the user to see. So, let's do that so i'm going to take this cipher statement that we wrote and add it to the subscribe to podcast mutation. So this is the mutation that we call when a user wants to subscribe to a podcast passes in the itunes id of the podcast. We then using cipher to define our custom logic here. Uh call out to the podcast index api, look that up by itunes id and then create what we merge. So we we get or create the podcast node set the details uh and these values we get from the podcast index and we create this user subscribes to podcast relationship and what we want to do right here. I'Ll paste in the cipher query that we just wrote, which is then going to parse that xml feed and start adding episodes uh. Let'S see, let's make sure we did this right, so um, [, Music, ], a user. I guess we don't need to match unp we've already merged on the podcast node. We'Ll call, i think we need, let's drop the user. I think we need in between a merge and a call statement. We need a width just to be explicit about what we want to pipe through there, so we'll say, with p, so only bring through the podcast node unwind and start parsing, oh and then because we're in a cipher directive field. I think we need to escape our double quotes here. So, let's escape all of those. I think we have yep two more over. I here do a find and replace. I guess, but we don't have that many here here and here and let's see if that works. So we'll save that, let's go back to our graphql api. I guess we need to search, say, podcast, search, uh, search term search for the accidental tech podcast. That'S that's another good one! Let'S get the title and itunes id cool. So now, let's subscribe to this one! So we're going to do a mutation subscribe to podcast, passing in the itunes id and then that returns the podcast object. Let'S make sure we don't have this one in the database, so we have just no. We just have graphistania and connected. So, let's run this and okay, it returns the podcast. So now, if we go to the database to refresh, we can see okay, here's the accidental tech podcast, so we added that to the database. If we traverse out, we then see episode nodes that we added so so jenny cat subscribes to the accidental tech podcast and then here are a bunch of episodes that we parsed neat cool. Although i don't see, i don't see a summary on these um. Let'S go back to the feed url all right. Let'S look at the feed url for this one, so the summary should just be in a summary element: let's zoom in a bit, do we have summary not as an element. These are just uh. Those are just the use of the word summary um. Let'S see is that is summary, not one of the required elements, so text input elements of item description - oh maybe description - is what we should have used instead of summary description. Yes, if we change this to description and then let's subscribe to the podcast again, that's okay, it'll, just uh merge over everything without creating duplicates. Let'S refresh here, look at some atp podcast episodes and now cool. We grab the description field in this case, atp publishes the description field as html, which is fine, that's good. We can just embed that html when we start building our fronted application cool, so that is podcast episodes um. What else do we need to do? I guess we need to [ Music ] deal with the playlists right, so we want to create a playlist and then we want to uh add episodes to a playlist view. Our subscribed uh view view episodes that we've added to the playlist view our feed. Oh, maybe let's do that. First, is our uh like episode feed right, so i i subscribe to podcasts and i just want to see my feed so of all the podcasts. I subscribe to show me the most recent episodes across all of those. Maybe that's what we want to do first, um! So let's create a new query field. Let'S call this episode feed, let's paginate this so we'll bring in uh first and offset um so making these required. Let'S, let's give these default values um and offset so by default. Let'S return the first 10. and these are going to return podcast episodes and because we're we only want to show the feed for the currently authenticated user, we're going to define our logic using the cipher schema directive, so we can take advantage of that user. Params object. Remember last time we added this sort of middleware here, where we parse the auth token, that's passed in the graphql request. So that's this token. We verify that token and we grab the user id out of that token. So this user id value is available in any of the cipher queries that i attach in my graphql schema in the cipher params object. So we're going to use that here so statements, so this is going to what match on the authenticated user. Let'S look them up by id and then i guess we're going to look for any podcast that this user subscribes to and then we want any episodes that the podcast has and we're going to return those episodes ordered by pub date descending. We could parametrize this. I don't know if there's a use case. I think we typically want our most recent episodes in the feed um [ Music ], we'll then skip um skip offset limit first skip limit yep. If we save that, i think we still need to add the episode type, though so i think that's the error. We'Re getting here that doesn't know. Yeah unknown type episode doesn't know what an episode is. So, let's say type episode. Uh episode has an id a pub date. That'S a date. Time object. What else we add a summary title link the image: url um, the audio file just called that audio, and then it's going to be connected to a podcast. Let'S, let's add that, so we use the relation directive to include the name of the or the relationship type they're called, not the name of the relationship type, which is has episode that connects a podcast, an episode and the direction is coming in because it's coming from The podcast to the episode okay. So now, if i refresh graphql playground to pick up the most recent type defs, i should see episode feed now notice, also that it's added episode and podcast query and mutation. I guess we had. We had podcasts before it now has episode, but now it has we've added the mutations to create relationships between episode and podcast uh. Okay, so i should be able to say feed episode, feed um, let's not we'll just use the default first 10.. I should get title summary and then i want to know the name uh the podcast and then let's also grab the pub date, so we can make sure that it is in the right order and cool. So this is episode, title 409 and remember. The summary in this case was in html. That'S why that looks like that. So this is an episode of accidental tech, podcast from 12 17. That'S today and then an episode of connected from yesterday and then 408 feature headphones, which is from excellent tech podcast from a week ago and so on cool. So if we think of uh in our our client application once we we build the front end for this. What we want to do is like show the user, their feed uh, where they're seeing okay, here's all the new episodes that i have and then maybe i don't want to listen to them now, but i want to add an episode to a playlist. So i, like click on one of the episodes in the list, add to playlist and then it's added to the playlist and later on. I can view and listen to the episodes in that playlist. So, let's, let's create the functionality for a user to add a playlist. I'M going to go down to the mutation type and i'm going to again define a custom mutation where the logic is using a cipher directive rather than take advantage of the generated mutation. So if i just add a playlist type up here, let's go ahead and do that so type playlist. What is a playlist gon na? Have, i guess, it'll have a name. I guess that's really. All it needs right is a name and then it'll have episodes and episodes are connected to playlists through a relationship called in playlist, let's say, and the direction is going to be coming in from the episode to the playlist. So if i do this so i'll save that to refresh the api i'll go over to playgrounds, refresh playground and now i have a playlist query field. That'S added and i also have a create delete, add playlist episode, so i can connect playlist to episode nodes. So i have all of those generated crud operations for the playlist just by creating its type here, but i don't actually want to expose those, because a playlist is specific to a user right. So i don't want to just add these and let any user access the playlist one way we could handle. This is to use the graphql authorization directives uh. If we've seen those let's go in the docs to authorization yeah. So we could start adding authorization rules in uh in our schema drop a link to that in the chat um. That would be one way to address this. What i want to do, though, is i'm just going to define the logic in cipher so that we can exclude these types, the playlist, similar to how we excluded the the user and the auth token, because we don't want those to be included in the generated mutations And queries so i'll add playlist to the exclude configuration here now. If i go back back to playgrounds refresh look at the docs playlist query field is gone as well as the playlist mutations. Okay, so we've defined the playlist type. Let'S add a create playlist mutation field. It'S going to take a name argument and return: the playlist that it just created and decipher statement. So what this is going to create a playlist node and then connect it to the currently authenticated user to say that the user? I think what did we say in our data model? Do we still have that here? I think we said: owns a user yeah, a user owns a playlist. I think instead of has episode, let's say in playlist yeah, so something like that um okay. So, let's first match on the authenticated user, so look them up, cipherprams dot user id and then let's merge this playlist okay. This is going to merge on on the whole pattern here. So if this user does not own a playlist of this name, it will create a new playlist and relationship, and then we return the playlist okay, so we can create a playlist. The next thing we want to do, then, is add an episode to it add episode to playlist. We need the name of the playlist and we need the id of the podcast and, let's return the playlist. I guess from this mutation. I guess that makes the most sense, so we need to match on again. We we want to make sure that we're using the currently authenticated user. So we want to match on the user by id looking up cipherprams.userid where the user owns this playlist. Look it up by name, then we want to match the podcast episode. So look that up by id and remember these graphql field arguments that the user passes at query time. Those are passed into the annotated, cipher statement here as parameters as cipher parameters and then we'll do a merge to avoid creating duplicates to merge this relationship. That says, the episode is in playlist and return the playlist uh okay. Should we test this? Let'S test it? So here's my episode feed i'm gon na i'm gon na remove the summary field. So it's a bit easier for us to to see here. So i'm in the app i open. The app gives me the episode feed. So here's all of the podcasts um that i see oh, i guess i need the the id for the podcast um and i see okay, here's this episode of accidental tech podcast. I want to listen to that uh on. I don't know my my next road trip. So now what i'm going to do is call a mutation first. I guess i need to create the playlist call this the road trip playlist and it just returns. The playlist, which just currently just has a name and now i want to do another mutation, which is add episode to playlist, so the name of the playlist is the one i just created road trip um. Oh, i called this podcast id it's more like uh episode, id yeah, whatever that's good enough, and what does this return? This returns the playlist um. What is we have a little error here? What did we do wrong? An extra? Oh, we had a missing curly, brace. Okay, so let's run that and cool now it says: here's your playlist called road trip. It has this episode um, let's add another episode to it: go back to our podcast feed uh, here's an episode of connected now. Let'S add that to our road trip, uh playlist. So we'll pass in the episode id there and cool now i've got two episodes. If i look in the database to see what's going on in the database, let's, let's close these frames just refresh to close all those frames there, we don't need to keep those rendered. If we look in the database, we have a playlist node called road trip, it says jenny, cat owns road trip and there's two episodes one of accidental tech podcasts one of connected in this playlist cool. So i think the other thing that's missing is like a view. All my playlists right, because here here i could see these episodes were in the playlist, but that was just as a direct uh result from the mutation. I probably need like uh in my app i'm going to want to have like view playlists and then select playlist, to view the the episodes for each playlist. So if we go up to the query type, let's add another query field: let's just call this playlists. So these this is going to return the playlists for the currently authenticated user. So again, we're gon na match on the user, looking them up by id and we're gon na find any playlists that they own playlist and we'll just return the playlist. So the nice thing about the cipher generation logic in the neo4j graphql js library is, i don't need to project out everything that i want to return in the selection set from this playlist query field. So i'm going to say oops i want curly brace, not brackets. I'M going to say playlist give me the name of the playlist, so that's kind of like this cipher statement but, of course the playlists. Well, they have episodes and episodes have title summary: [, Music, ], audio they're, connected to the podcast. That has a title description right. So i can, i can build up more complex selection set, but i don't need to specify all that in this query, because uh, your geographical js, the library we're using to generate our graphql resolvers, knows how to generate the cipher statement to fetch all of the additional Fields in the selection set, if we look at the cipher query that was generated well, this is it here cool. Well now i've got my playlist here's road trip, so i can grab the most recent episodes on this road trips playlist and here's the audio file for the playlist that i'm interested in or the episode i'm interested in, and i can start listening to that. So i think that is everything that we wanted to cover. Today we figured out how to parse podcast feeds using cipher. So we added the logic here to our subscribe to podcast mutation, that when a user subscribes to a podcast to go out and fetch the podcast feed url, add the episodes to the database. We also added the concept of playlists to our graphql api, so a user can create a playlist, they can add episodes to it. All of that information is private and specific to that user. Using the user authentication functionality that we built last time cool. So i think we will um, i think, we'll call it there. So i'll push up the code for all this to github again, the link is right here, drop that link in the chat. I'Ve also been been writing up. Each of these live stream episodes as blog posts. So, for example, you can find episode 2 that we wrote up here as well, if you prefer to consume that in blog form, basically showing last time how we added uh, authentication functionality, cool um, i'll push up the code and [ Music ] check out the code. If you're interested, i've also recently created uh a newsletter i'll drop a link in the chat to that. If you want to get these sort of updates in your uh in your inbox, just posting links to blog posts, things i'm working on um. That kind of thing! So we want to keep track, that's a good resource as well cool. So this is my last live stream of the year. I think, since our our next two regular thursday times fall on christmas eve and and new year's eve, so we'll pick back up at the beginning of next year, the first week in january, and i think i think it's time to start building out some of the Front end of this application, we've built out the graphql api um to the point where we have some good functionality, but now we need to think about the front end of our application and i think that we'll take a look at using next.js to do this. If you're not familiar with nexjs, it is a framework built on top of react, so kind of like a batteries included framework built on top of react. So that's what we'll start with next time, um first week in january, building out the front end for this application. In the meantime, if you uh, if you have any thoughts on features, you'd like to see added, feel free to, let me know on twitter. I think it's probably the easiest way. I'Ll drop a link to to my twitter here, just ping me on twitter with any feature, suggestions that you'd like to see us uh working on in these live streams, um, otherwise, hope everyone has a good end of the year and we will see you uh after The holidays and start working on the front end, which should be pretty fun because we'll actually then be able to start listening to uh the podcasts uh that we're subscribing to cool. Well thanks a lot for joining today and we'll see you next time. Bye, you
Subscribe To Will's Newsletter
Want to know when the next blog post or video is published? Subscribe now!