Using a RSS feed to make a Podcast Skill

Hello everyone, this tutorial is inspired by Will East’s request to use a RSS feed from Soundcloud with the stream block.

So here’s what we’re going to do in this tutorial:

  • Use an API block to transform the rss feed into JSON
  • Filter the information we want from this RSS feed with a Code block
  • Create the mechanics to be able to change episodes with the next and previous commands of the Stream block
  • And as a bonus, display the information on the Alexa device with the Display block.

As usual, here is an overview of the skill we will create:


So let’s start by creating a new skill in Voiceflow by clicking on the + or the “New Project” button at the top right of your screen. For this tutorial I named mine “Stream Radio Demo


01%20PM

The first thing we will do in this new skill is to create the local variables that we will use next.
So click on the Variables button:

19%20PM

and in the Local tab, add the following variables

Do do so, just add the variable name in the field and click the + button.

Here is the full list of the variables you need to add to your skill:

j_stream
urls
titles
thumbnails
authors
descriptions
cTitle
cAuthor
cUrl
cThumb
cDescription
current
maxFeed

Once all the variables have been added, we will move on to the first step, converting an RSS feed to JSON. Indeed Will East provided me with an RSS link that comes from the Soundcloud platform, so we need to convert this link so that it can be read from Voiceflow.
To help us in this task, I use the API of an online service: rss2json.com

It’s very easy to use and it does exactly what you expect it to do.

So, after a first welcome message, we will add an API block to our skill.

image

The configuration of the block will be quite simple, in the Endpoint URL we will add the url of the rss2jon api and the url of the RSS feed as the rss_url param.

In our example, it looks like this:

https://api.rss2json.com/v1/api.json?rss_url=https%3A%2F%2Ffeeds.soundcloud.com%2Fusers%2Fsoundcloud%3Ausers%3A143582161%2Fsounds.rss

The rss2jon part: https://api.rss2json.com/v1/api.json?rss_url=

and the RSS feed url: https%3A%2F%2Ffeeds.soundcloud.com%2Fusers%2Fsoundcloud%3Ausers%3A143582161%2Fsounds.rss

I know, you’re going to tell me: what’s with all the weird characters in the RSS feed url?

And I would answer you: most of the time you have to encode your parameters in a request.

So let’s do this together! The url provided by Will East was: http://feeds.soundcloud.com/users/soundcloud:users:143582161/sounds.rss
so first, we need to check that this url is accessible from https (without this secure layer you will not pass Alexa certification). So let’s test this url with https instead of http in our browser: https://feeds.soundcloud.com/users/soundcloud:users:143582161/sounds.rss

You should have something like this

This is the RSS feed. We realize two things: the url in https works and now you understand better why we’re going to have to convert all this :wink:

So far so good, now all you have to do is copy/paste this address after our API call:

https://api.rss2json.com/v1/api.json?rss_url=https://feeds.soundcloud.com/users/soundcloud:users:143582161/sounds.rss

Last thing for the API block, map the response to the j_stream variable

To see the magic in action, use the “Test Endpoint” function in the API block.

Tada! The RSS feed is now a JSON response with which we will be able to play

We’re going to get to the heart of the matter, so if you want to take a break it’s now :slight_smile:


Now, the Code block, or as I like to call it… The Magic Box

Ok, so we have our JSON which contains all the information from RSS feeds we will have to navigate inside to retrieve all the information we are interested in. The title of the podcast, the author, the thumbnail, the description and above all, the link to the audio file.

All this could be very complicated but fortunately we have our Code block.

So let’s add a Code block code just after the API block we just finished.

jou

Take a deep breath and without thinking, copy all the following code and paste it into the Code block.

var Utf8 = {
	// public method for url encoding
	encode : function (string) {
		string = string.replace(/\r\n/g,"\n");
		var utftext = "";
		for (var n = 0; n < string.length; n++) {
			var c = string.charCodeAt(n);
			if (c < 128) {
				utftext += String.fromCharCode(c);
			}
			else if((c > 127) && (c < 2048)) {
				utftext += String.fromCharCode((c >> 6) | 192);
				utftext += String.fromCharCode((c & 63) | 128);
			}
			else {
				utftext += String.fromCharCode((c >> 12) | 224);
				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
				utftext += String.fromCharCode((c & 63) | 128);
			}
		}
		return utftext;
	},
	// public method for url decoding
	decode : function (utftext) {
		var string = "";
		var i = 0;
		var c = c1 = c2 = 0;
		while ( i < utftext.length ) {
			c = utftext.charCodeAt(i);
			if (c < 128) {
				string += String.fromCharCode(c);
				i++;
			}
			else if((c > 191) && (c < 224)) {
				c2 = utftext.charCodeAt(i+1);
				string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
				i += 2;
			}
			else {
				c2 = utftext.charCodeAt(i+1);
				c3 = utftext.charCodeAt(i+2);
				string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
				i += 3;
			}
		}
		return string;
	}
}

urls = [];
titles = [];
thumbnails = [];
authors = [];
descriptions = [];

var tmp_title = "";
maxFeed = 0;
for(var a in j_stream.items) {
	tmp_title = Utf8.decode((j_stream.items[a].title).replace(/ +(?= )/g,''));
	titles[a] = tmp_title.substr(tmp_title.indexOf(":")+1, tmp_title.length);
	titles[a] = titles[a].trim();
	authors[a] = Utf8.decode((j_stream.items[a].author).replace(/ +(?= )/g,''));
	thumbnails[a] = j_stream.items[a].thumbnail;
	thumbnails[a] = thumbnails[a].replace("http","https");
	descriptions[a] = Utf8.decode((j_stream.items[a].description).replace(/ +(?= )/g,''));
	urls[a] = j_stream.items[a].enclosure.link;
  maxFeed ++;
}

Don’t panic, the advantage of Voiceflow is that you can do magic without being a magician

So I’m not going to go into the code in detail but to make it simple, this part is used to decode certain characters that may be present in the title or description.

This is just to impress you, it’s the smoke the magician uses in his tricks, don’t pay attention to it :slight_smile:

var Utf8 = {
    	// public method for url encoding
    	encode : function (string) {
    		string = string.replace(/\r\n/g,"\n");
    		var utftext = "";
    		for (var n = 0; n < string.length; n++) {
    			var c = string.charCodeAt(n);
    			if (c < 128) {
    				utftext += String.fromCharCode(c);
    			}
    			else if((c > 127) && (c < 2048)) {
    				utftext += String.fromCharCode((c >> 6) | 192);
    				utftext += String.fromCharCode((c & 63) | 128);
    			}
    			else {
    				utftext += String.fromCharCode((c >> 12) | 224);
    				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
    				utftext += String.fromCharCode((c & 63) | 128);
    			}
    		}
    		return utftext;
    	},
    	// public method for url decoding
    	decode : function (utftext) {
    		var string = "";
    		var i = 0;
    		var c = c1 = c2 = 0;
    		while ( i < utftext.length ) {
    			c = utftext.charCodeAt(i);
    			if (c < 128) {
    				string += String.fromCharCode(c);
    				i++;
    			}
    			else if((c > 191) && (c < 224)) {
    				c2 = utftext.charCodeAt(i+1);
    				string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
    				i += 2;
    			}
    			else {
    				c2 = utftext.charCodeAt(i+1);
    				c3 = utftext.charCodeAt(i+2);
    				string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
    				i += 3;
    			}
    		}
    		return string;
    	}
    }

This part, on the other hand, is much more interesting.

maxFeed = 0;
    for(var a in j_stream.items) {
    	tmp_title = Utf8.decode((j_stream.items[a].title).replace(/ +(?= )/g,''));
    	titles[a] = tmp_title.substr(tmp_title.indexOf(":")+1, tmp_title.length);
    	titles[a] = titles[a].trim();
    	authors[a] = Utf8.decode((j_stream.items[a].author).replace(/ +(?= )/g,''));
    	thumbnails[a] = j_stream.items[a].thumbnail;
    	thumbnails[a] = thumbnails[a].replace("http","https");
    	descriptions[a] = Utf8.decode((j_stream.items[a].description).replace(/ +(?= )/g,''));
    	urls[a] = j_stream.items[a].enclosure.link;
      maxFeed ++;
    }

It’s what we call a loop, it’s the moment when the magician shuffles his cards.
So what we do here is that for each item present in the RSS feed, we will retrieve the title, author, thumbnail, description and url and we will store it in a variable (here an array).
And for each pass in the loop, we add 1 to our maxFeed variable. This way, once out of this loop, our maxFeed variable will contain the number of audio files available. Same for our arrays, titles[0] will contain the first title, titles[1] the second, titles[2] the third… you get it.

That’s all for the Magic Box but if you want to learn more or have any questions, feel free to post them in the comments.


The next step is again a Code block but this one is much easier to understand, so add a new Code block just after the one you just finished

25%20PM

and add the following code inside:

if(current < 0){
  current = maxFeed-1;
}
if(current > maxFeed-1){
  current = 0;
}

cTitle = titles[current];
cAuthor = authors[current];
cThumb = thumbnails[current];
cDescription = descriptions[current];
cUrl = urls[current];

Here we check if we are at the end of our list of audio files and we start from the beginning if this is the case. This is what allows us to easily use the Next and Previous commands in the Stream block.

The second part allows us to pass all the information of the selected title in our variables so that we can use it just after in our Speak block and Display block.


So let’s add a Speak block with the variable {cTitle} as content


Let’s now move on to the Display block, add it just after the Speak block.

19%20PM

In the Display block settings, click on “Add Displays

Then on “New Display

You can give it any name you want, I chose “Radio Stream”.
Remove the Write/Paste APL Document Here and hit “Save

Copy the following code in the left part (APL Document)

{
	"type": "APL",
	"version": "1.0",
	"theme": "dark",
	"import": [
		{
			"name": "alexa-layouts",
			"version": "1.0.0"
		}
	],
	"resources": [
		{
			"description": "Stock color for the light theme",
			"colors": {
				"colorTextPrimary": "#151920"
			}
		},
		{
			"description": "Stock color for the dark theme",
			"when": "${viewport.theme == 'dark'}",
			"colors": {
				"colorTextPrimary": "#f0f1ef"
			}
		},
		{
			"description": "Standard font sizes",
			"dimensions": {
				"textSizeBody": 48,
				"textSizePrimary": 22,
				"textSizeSecondary": 23,
				"textSizeSecondaryHint": 25
			}
		},
		{
			"description": "Common spacing values",
			"dimensions": {
				"spacingThin": 6,
				"spacingSmall": 12,
				"spacingMedium": 24,
				"spacingLarge": 48,
				"spacingExtraLarge": 72
			}
		},
		{
			"description": "Common margins and padding",
			"dimensions": {
				"marginTop": 40,
				"marginLeft": 60,
				"marginRight": 60,
				"marginBottom": 40
			}
		}
	],
	"styles": {
		"textStyleBase": {
			"description": "Base font description; set color",
			"values": [
				{
					"color": "@colorTextPrimary"
				}
			]
		},
		"textStyleBase0": {
			"description": "Thin version of basic font",
			"extend": "textStyleBase",
			"values": {
				"fontWeight": "10"
			}
		},
		"textStyleBase1": {
			"description": "Light version of basic font",
			"extend": "textStyleBase",
			"values": {
				"fontWeight": "300"
			}
		},
		"mixinBody": {
			"values": {
				"fontSize": "@textSizeBody"
			}
		},
		"mixinPrimary": {
			"values": {
				"fontSize": "@textSizePrimary"
			}
		},
		"mixinSecondary": {
			"values": {
				"fontSize": "@textSizeSecondary"
			}
		},
		"textStylePrimary": {
			"extend": [
				"textStyleBase1",
				"mixinPrimary"
			]
		},
		"textStyleSecondary": {
			"extend": [
				"textStyleBase0",
				"mixinSecondary"
			]
		},
		"textStyleBody": {
			"extend": [
				"textStyleBase1",
				"mixinBody"
			]
		},
		"textStyleSecondaryHint": {
			"values": {
				"fontFamily": "Bookerly",
				"fontStyle": "italic",
				"fontSize": "@textSizeSecondaryHint",
				"color": "@colorTextPrimary"
			}
		}
	},
	"layouts": {},
	"mainTemplate": {
		"parameters": [
			"payload"
		],
		"items": [
			{
				"when": "${viewport.shape == 'round'}",
				"type": "Container",
				"direction": "column",
				"width": "100vw",
				"height": "100vh",
				"items": [
					{
						"type": "Image",
						"width": "100vw",
						"height": "100vh",
						"source": "${payload.bodyTemplate3Data.backgroundImage.sources[0].url}",
						"scale": "best-fill",
						"overlayColor": "rgba(0, 0, 0, 0.6)",
						"position": "absolute"
					},
					{
						"type": "ScrollView",
						"width": "100vw",
						"height": "100vh",
						"item": [
							{
								"type": "Container",
								"direction": "column",
								"alignItems": "center",
								"paddingLeft": 30,
								"paddingRight": 30,
								"paddingBottom": 200,
								"items": [
									{
										"type": "AlexaHeader",
										"headerAttributionImage": "${payload.bodyTemplate3Data.logoUrl}",
										"headerTitle": ""
									},
									{
										"type": "Text",
										"style": "textStylePrimary",
										"width": "90vw",
										"textAlign": "center",
										"color": "#edeff7",
										"text": "<b>${payload.bodyTemplate3Data.title}</b>"
									},
									{
										"type": "Text",
										"text": "${payload.bodyTemplate3Data.textContent.subtitle.text}",
										"style": "textStylePrimary",
										"width": "90vw",
										"textAlign": "center"
									},
									{
										"type": "Text",
										"text": "${payload.bodyTemplate3Data.textContent.primaryText.text}",
										"paddingTop": 40,
										"style": "textStylePrimary",
										"width": "90vw",
										"textAlign": "center"
									}
								]
							}
						]
					}
				]
			},
			{
				"type": "Container",
				"width": "100vw",
				"height": "100vh",
				"items": [
					{
						"type": "Image",
						"source": "${payload.bodyTemplate3Data.backgroundImage.sources[0].url}",
						"scale": "best-fill",
						"width": "100vw",
						"height": "100vh",
						"position": "absolute"
					},
					{
						"headerAttributionImage": "${payload.bodyTemplate3Data.logoUrl}",
						"type": "AlexaHeader"
					},
					{
						"type": "Container",
						"direction": "row",
						"paddingLeft": 40,
						"paddingRight": 72,
						"grow": 1,
						"items": [
							{
								"type": "Image",
								"width": "340",
								"height": "360",
								"paddingRight": "80",
								"source": "${payload.bodyTemplate3Data.image.sources[0].url}",
								"scale": "best-fit",
								"align": "center"
							},
							{
								"type": "ScrollView",
								"height": "60vh",
								"shrink": 1,
								"item": [
									{
										"type": "Container",
										"items": [
											{
												"type": "Text",
												"style": "textStylePrimary",
												"color": "#edeff7",
												"text": "<b>${payload.bodyTemplate3Data.textContent.title.text}</b>"
											},
											{
												"type": "Text",
												"text": "${payload.bodyTemplate3Data.textContent.subtitle.text}",
												"style": "textStylePrimary"
											},
											{
												"type": "Text",
												"text": "${payload.bodyTemplate3Data.textContent.primaryText.text}",
												"paddingTop": 40,
												"style": "textStylePrimary"
											}
										]
									}
								]
							}
						]
					}
				]
			}
		]
	}
}

And this code in the right part (Default Datasource)

{
    "bodyTemplate3Data": {
        "type": "object",
        "objectId": "bt3Sample",
        "backgroundImage": {
            "contentDescription": null,
            "smallSourceUrl": null,
            "largeSourceUrl": null,
            "sources": [
                {
                    "url": "https://www.supertalk.fm/wp-content/uploads/2017/11/IMG_0024.png",
                    "size": "small",
                    "widthPixels": 0,
                    "heightPixels": 0
                },
                {
                    "url": "https://www.supertalk.fm/wp-content/uploads/2017/11/IMG_0024.png",
                    "size": "large",
                    "widthPixels": 0,
                    "heightPixels": 0
                }
            ]
        },
        "title": "Radio",
        "image": {
            "contentDescription": null,
            "smallSourceUrl": null,
            "largeSourceUrl": null,
            "sources": [
                {
                    "url": "{cThumb}",
                    "size": "small",
                    "widthPixels": 0,
                    "heightPixels": 0
                },
                {
                    "url": "{cThumb}",
                    "size": "large",
                    "widthPixels": 0,
                    "heightPixels": 0
                }
            ]
        },
        "textContent": {
            "title": {
                "type": "PlainText",
                "text": "{cTitle}"
            },
            "subtitle": {
                "type": "PlainText",
                "text": "{cAuthor}"
            },
            "primaryText": {
                "type": "PlainText",
                "text": "{cDescription}"
            }
        },
        "logoUrl": "https://www.supertalk.fm/wp-content/themes/supertalk-2017-Update/images/footerlogos-02.jpg",
        "hintText": ""
    }
}

Hit “Save” again.

Your screen should look like this:

Again I will not go into too much detail because the APL part alone would require a complete tutorial but to make it simple, the left part (APL Document) is the structure of the document while the right part (Datasource) contains all the data to display.

If you really want to go further with the APL language, you can start by visiting the APL Authoring Tool

But just so you know, you can change the background by changing these urls for example

And this is where you can change the logo url

And here is a preview of what this APL code looks like for human :slight_smile:

Going back to the Canvas now

In the Display block select the template you just created

Then click the “Update On Variable Changes” checkbox

That’s it, we’re done with the Display block.


Let’s add the Stream block now

09%20PM

Maybe the easier part here, click on the URL button

Add {cUrl} in the audio url field and hit “Confirm

That’s it for the Stream block!


Last step, we will add two Set blocks to increase or decrease our {current} variable (this is what allows us to move to the next or previous audio file).

So let’s start with the first Set block

This one will add 1 to our {current} variable





Here is a link to a quick video on how to setup this Set block:

For the second Set block, just select the one you just made and press ctrl + c on a PC or command + c on a Mac copy it.

secondblock

Now, press ctrl + v or command + v to paste it on your canvas.

Change the + to a - and rename it to “Current -1” for example


Last thing, link the next output from the Stream block to the Current +1 Set block, the previous output to the Current -1 and the Set blocks outputs to the Loop Code block.


Congratulations, you have just created a podcast skill!

Keep in mind that any project using the Stream block can’t be tested on ADC as Amazon don’t support stream playback for now. You need to test it on a real device or the Alexa app on your phone.

As usual, feel free to comment if you have any comments or questions.
Hope you enjoyed this tutorial and see you soon!

I dont understand the part of display block.

It’s where we are using APL code to display some informations we get from the RSS feed on devices with screen. But this part is not mandatory to test the skill so you can ignore the display block if it’s something you didn’t get quite well for now.

I want to understand it, because i made a skill whit api.google, but i don’t know.how to make the apl code to see results…

For now you can’t use the Display block for Google Action. But you can replace this with a Card block.

Thank for the helpful tutorial. I’m creating my own podcast skill in just 30 min from scratch:) But it works with Alexa only. Can you have any ideas about why it is? After API block processing skill show podcast name and Exiting Flow without playing. Maybe it’s helpful - I create RSS from iTunes podcast with http://getrssfeed.com/

You mean it’s working on Alexa devices but not in the Voiceflow simulator?

I have built a skill with this tutorial. I have noticed that my echo gets a red ring while playing the podcast. It continues playing but you can’t stop the skill, skip to the next, or previous audio.

Did you use the “Alexa” wake word before calling the next or previous?

There is no other way to ask for next, or previous. You must say alexa, next. It works, but not always. With the red ring you can’t even stop the skill.

“It works, but not always” < this is strange… Are you using a Sonos One? I got the same problem with Sonos devices.

Ok, got my answer from the “red ring” thing :slight_smile:

No, I am using a regular echo.

OK, working fine for me on an Echo with US-en skill. I will give it a try tomorrow on a French version.

Thanks for the awesome tutorial.
Titles in my podcast feed start with an episode number and a colon, something like Episode 1:
Voiceflow seems to strip that part out. Is it possible to keep it? Thanks.

Sure, just replace this line in the Parse info code block:

titles[a] = tmp_title.substr(tmp_title.indexOf(":")+1, tmp_title.length);

With this line:

titles[a] = tmp_title;

1 Like

Thank you @Nicolas, I am a newbie and your help is much appreciated. Will you please elaborate a bit on the fail blocks? What type of blocks are they? What’s their contents? Thanks.

The fail block are just speak blocks with error messages describing the type of error in case of a fail from this block.

As far as I get it, this is a basic podcast skill with basic commands (next/previous and stop/resume) that upon invocation automatically plays the latest episode. What are in your experience other podcast skill commands that can improve the listener experience?

I’m having problems with the TEST on Alexa. This is the error

Error retrieving device rendering

The object provided is not a valid template