Building a podcasting app in Angular 6 — Part 2: Loading and playing podcasts

In the last part we created a service and component to search the iTunes API for podcasts.

Now we’re going to have a look at how we can open the podcasts from the search results. We’re going to go through the following topics:

  • Setting up the router;
  • Using an API to load RSS feeds;
  • Creating the podcast component with an episode list and player.

The project is available on Github here and the commit for this part here.

Setting up the Router:

To load different components in our application, we need a Router so we can configure which component belongs to which path. For example, we might have the path ‘/search’ associated with the SearchComponent we built in the last article or a ‘podcast/id’ associated with a PodcastComponent that will display the podcast info and episodes.

First, we need to import the RouterModule and configure the routes we want to be associated with the Router in our app.module.ts

...
import { Routes, RouterModule } from '@angular/router';
const appRoutes: Routes = [
{ path: 'search', component: SearchComponent },
{ path: '', pathMatch: 'full', redirectTo: '/search'}
{ path: '**', redirectTo: '/search' }
];
@NgModule({
declarations: [
AppComponent,
SearchComponent
],
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
HttpClientJsonpModule,
RouterModule.forRoot(appRoutes)
],
providers: [
SearchService,
PodcastService
],
bootstrap: [AppComponent]
})
export class AppModule { }

Our appRoutes have three entries:

  • {path: 'search', component: SearchComponent} connects the SearchComponent to the path /search
  • { path: '', pathMatch: 'full', redirectTo: '/search'} redirects the empty url to the path /search, so when a user visits the base url they will get redirected to the /search
  • { path: '**', redirectTo: '/search'} matches any path and redirects it to the /search path. This could be used to show a not found page but we’ll just redirect to the search because it’s what we have now.

In our imports we imported RouterModule.forRoot(appRoutes) . This function returns a module with the Router service provider configured with our appRoutes.

Now, all we have to do is use the router outlet directive in our app.component.ts:

<router-outlet></router-outlet>

The Router will replace this directive with whatever component matches the current path. Now if you go to localhost:4200 (if your app isn’t running do it now with the command $ ng serve ) you should be redirected to localhost:4200/search and the SearchComponent will be displayed.

The podcast service

Now that we have our router setup, we can create our podcast service where we will request and load podcasts for the components to use.

First, let’s define our models. You can check the W3 RSS specification here for all the fields but I have included the important ones here. We need a model for the API return, which we will call PodcastFeed, and a model for each episode in the Feed, which will be PodcastEpisode. Both these files will go inside the app/models folder we created in the previous article. In our podcast-episode.model.ts we will have:

export interface PodcastEpisode {
author: string;
categories: string[];
content: string;
description: string;
enclosure: Enclosure;
guid: string;
link: string;
pubDate: Date;
thumbnail: string;
title: string;
}
export interface Enclosure {
link: string;
type: string;
length: number;
duration: number;
thumbnail: string;
}

Here, the Enclosure represents the actual file corresponding to the podcast episode. Now let’s define our PodcastFeed in the podcast-feed.model.ts:

import { PodcastEpisode } from './podcast-episode.model';
export class PodcastFeed {
feed: {
author: string;
description: string;
image: string;
link: string;
title: string;
url: string;
};
items: PodcastEpisode[];
constructor( response ) {
this.feed = response.feed;
this.items = response.items;
}
}

Now that our models are defined, we can create the serivce and implement it. As before, let’s use the Angular CLI to generate the service: $ ng g s services/podcast In order to make things easier for us, we’ll use the rss2json API that converts RSS feeds to JSON with one call to their API. This saves us the effort of having to do it ourselves. First let’s implement a function that gets a podcast given it’s feed url. Our podcast.service.ts should look something like this:

import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { PodcastFeed } from '../models/podcast-feed.model';
@Injectable({
providedIn: 'root'
})
export class PodcastService {
constructor(private http: HttpClient) { }
getPodcast(feedUrl: string) {
const url = 'https://api.rss2json.com/v1/api.json?rss_url=' + feedUrl;
return this.http.get(url).pipe(
map(res => new PodcastFeed(res))
);
}
}

Here we use the native javascript function encodeURI to make sure it’s an allowed query param for our request. If you want more control over the results you’ll need to get an API key from the rss2json website, this, however, is enough for the purpose of this series. (Note: the rss2json API has some limitations, it can only show a number of entries, 10 without an API key, and the free tier is limited to 50 feeds, I haven’t reached the limit so I’m unaware of what will happen when you do)

Podcast Component

Now that we can get a podcast’s feed, we have to create a component to display it. Using the Angular CLI $ ng g c components/podcast we generate the PodcastComponent in the components folder. In order to load the podcast, we need to pass it in the Router parameters, this means we need to pass some identifier from which we can get the podcast feed. In order to get the parameters from the route url, we will need to access the ActivatedRoute so it needs to be injected into our component. On initialization, we get the parameters from the ActivatedRoute and the podcast from the PodcastService so our podcast.component.ts looks like:

import { PodcastFeed } from './../../models/podcast-feed.model';
import { PodcastService } from './../../services/podcast.service';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-podcast',
templateUrl: './podcast.component.html',
styleUrls: ['./podcast.component.scss']
})
export class PodcastComponent implements OnInit {
feed: PodcastFeed;
constructor(private route: ActivatedRoute, private podcastService: PodcastService) { }
ngOnInit() {
this.route.queryParamMap.subscribe(params => {
this.podcastService.getPodcastFromEncodedUri(params.get('url')).subscribe( feed => {
this.feed = feed;
});
});
}
}

Now that we have the feed in the podcast component, we can display the episode listing where we will eventually play them. For now, we can keep it simple by displaying the podcast title and a list of the episode. Our podcast.component.html can look something like:

<h1>
{{this.feed?.feed.title}}
</h1>
<p>
{{this.feed?.feed.description}}
</p>
<audio id="player" controls>
<source id="player-source" src="" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<h2>Episodes:</h2>
<ul>
<li *ngFor="let item of this.feed?.items" >
<p (click)="playEpisode(item)">{{item.title}}</p>
</li>
</ul>

In order to play the episode, we have to add the playEpisode function to the podcast.component.ts:

playEpisode(episode: PodcastEpisode) {
(document.getElementById('player') as HTMLAudioElement).src = episode.enclosure.link;
(document.getElementById('player') as HTMLAudioElement).play();
console.log('playing: ' + episode.enclosure.link);
}

We now have a simple podcast viewing page that allows us to play episodes by clicking on them and looks like this:

You still won’t be able to get to this page because the router hasn’t been configured for the new component and nothing links to it. All we have to do is add the PodcastComponent to the appRoutes in app.component.ts

const appRoutes: Routes = [
{ path: 'search', component: SearchComponent },
{ path: 'podcast', component: PodcastComponent },
{ path: '', pathMatch: 'full', redirectTo: '/search'},
{ path: '**', redirectTo: '/search' }
];

And we have to link to it from the search results in the SearchComponent. We can do this in two ways, by having a function that navigates to the ‘/podcast’ path this.router.navigate(['/podcast'], { queryParams: {url, podcast.feedUrl}})(the functionopenPodcast in the file search.component.ts is an example of this) or by adding it to the template (how it’s done in this example) so the search.component.html looks like:

<form (ngSubmit)="submitSearch()">
<input type="text" placeholder="Search" name="term" [(ngModel)]="term">
<button type="submit">Search</button>
</form>
<div *ngIf="!results">
<p>Search for something!</p>
</div>
<div *ngIf="results?.resultCount == 0">
<p>Couldn't find results</p>
</div>
<div *ngIf="results?.resultCount > 0">
<div *ngFor="let res of results.results">
<a [routerLink]="['/podcast']" [queryParams]="{ url: res.feedUrl}">{{ res.trackName }} - {{ res.artistName }}</a>
</div>
</div>

All we had to do here was replace the <p> tag with an <a> tag with the bindings [routerLink], the destination path we specified in the routes above, and [queryParams] which contain the url of the podcast feed and now we can open up the podcast from the search results.

What’s next

Now that we have a simple player we can go on to subscribing to podcasts and a homepage that displays the podcasts we’re subscribed to. The app still looks bad but we’ll get to it soon. Again, let me know if you have any suggestions or questions.