Reputation: 133
I have a recursive JavaScript that I am using in angular 19 to pull data from YouTube. The script is working, but is there a better way to do it?
public getSeriesList() {
this.record.webTubeSeries = [];
this._seqNum = 0;
this.getUrlYouTube()
.pipe(first())
.subscribe({
next: (res: any) => {
this.parseVideoList(res["items"]);
if (res['nextPageToken']) {
let repeatGetNextVideoPage = (_token: string) => {
this.getNextVideoPage(_token)
.subscribe(
(result: any) => {
this.parseVideoList(result["items"]);
if (result["nextPageToken"]) {
repeatGetNextVideoPage(result["nextPageToken"]);
}
}
),
(err: any) => {
console.log("HTTP Error", err.message)
}
}
repeatGetNextVideoPage(res["nextPageToken"]);
}
}
}
);
}
private getNextVideoPage(_token: string) {
let url = (this.urlYouTube + this.videoListId + '&pageToken=' + _token);
return this.http.get(url);
}
private parseVideoList(result: any) {
for (let v of result) {
var item = new WebtubeSeries;
item.videoTitle = v.snippet.title;
item.videoId = v.snippet.resourceId.videoId;
if (v.snippet.thumbnails) {
item.urlThumbNailDefault = v.snippet.thumbnails.default.url;
item.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
item.urlThumbNailHigh = v.snippet.thumbnails.high.url;
item.widthDefault = v.snippet.thumbnails.default.width.toString();
item.heightDefault = v.snippet.thumbnails.default.height.toString();
item.widthMedium = v.snippet.thumbnails.medium.width.toString();
item.heightMedium = v.snippet.thumbnails.medium.height.toString()
item.widthHigh = v.snippet.thumbnails.high.width.toString()
item.heightHigh = v.snippet.thumbnails.high.height.toString();
item.seqNumber = this._seqNum++;
//item.id = item.seqNumber;
this.record.webTubeSeries.push(item);
}
}
}
Upvotes: -1
Views: 79
Reputation: 133
import { Component, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import { expand, takeWhile, tap, reduce, map } from 'rxjs';
import { CommonModule } from '@angular/common';
import { HttpClient, provideHttpClient } from '@angular/common/http';
export class WebtubeSeries {
videoTitle: any;
urlThumbNailDefault: any;
urlThumbNailMedium: any;
videoId: any;
urlThumbNailHigh: any;
widthDefault: any;
heightDefault: any;
widthMedium: any;
heightMedium: any;
widthHigh: any;
heightHigh: any;
seqNumber: number;
constructor(v: any, _seqNum = 0) {
this.seqNumber = _seqNum;
this.videoTitle = v.snippet.title;
this.videoId = v.snippet.resourceId.videoId;
if (v.snippet.thumbnails) {
this.urlThumbNailDefault = v.snippet.thumbnails.default.url;
this.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
this.urlThumbNailHigh = v.snippet.thumbnails.high.url;
this.widthDefault = v.snippet.thumbnails.default.width.toString();
this.heightDefault = v.snippet.thumbnails.default.height.toString();
this.widthMedium = v.snippet.thumbnails.medium.width.toString();
this.heightMedium = v.snippet.thumbnails.medium.height.toString();
this.widthHigh = v.snippet.thumbnails.high.width.toString();
this.heightHigh = v.snippet.thumbnails.high.height.toString();
this.seqNumber = _seqNum;
}
}
}
@Component({
selector: 'app-root',
imports: [CommonModule],
template: `
<h1>Youtube Videos List Title</h1>
<ul>
<li *ngFor="let s of youtube.value()">{{ s.seqNumber}} {{ s.videoTitle }}</li>
<ul>
`,
})
export class App {
private readonly YOUTUBE_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
private readonly YOUTUBE_URL = 'https://www.googleapis.com/youtube/v3/playlistItems?key=' + this.YOUTUBE_KEY +'&part=snippet&maxResults=12&playlistId='
videoListId = 'PLnR2Na1oFxkgk-ucqKSN1gQ2GJyz27cPw';
http = inject(HttpClient);
youtube = rxResource({
loader: () => {
let _seqNum = 0;
return this.getUrlYouTube().pipe(
expand((response: any) =>
this.getNextVideoPage(response['nextPageToken'])
),
takeWhile((response: any) => !!response['nextPageToken'], true),
reduce((all: any, data: any) => all.concat(data.items), []),
map((response: any) => {
return this.parseVideoList(response, _seqNum);
})
);
},
});
private getUrlYouTube() {
return this.http.get(this.YOUTUBE_URL + this.videoListId)
}
private parseVideoList(response: any, _seqNum: number) {
const result = [];
for (let v of response) {
_seqNum++;
result.push(new WebtubeSeries(v, _seqNum));
}
return result;
}
private getNextVideoPage(_token: string) {
let url = this.YOUTUBE_URL + this.videoListId + '&pageToken=' + _token;
return this.http.get(url);
}
}
bootstrapApplication(App,{
providers:[
provideHttpClient()
]
});
To test it, you need a key from Youtube. You can use my existing videolistId or you can use your own one. Thanks.
Upvotes: 0
Reputation: 55989
In angular19 they have introduced resource
and rxResource
which can be used for data handling based on input signals. I am suggesting this approach using rxResource
since we can leverage rxjs to perform the recursion.
Recursively calling an API using RxJS expand operator
First we define the construction of the class logic inside the class constructor.
export class WebtubeSeries {
videoTitle: any;
urlThumbNailDefault: any;
urlThumbNailMedium: any;
videoId: any;
urlThumbNailHigh: any;
widthDefault: any;
heightDefault: any;
widthMedium: any;
heightMedium: any;
widthHigh: any;
heightHigh: any;
seqNumber: number;
constructor(v: any, _seqNum = 0) {
this.seqNumber = _seqNum;
// this.videoTitle = v.snippet.title;
// this.videoId = v.snippet.resourceId.videoId;
// if (v.snippet.thumbnails) {
// this.urlThumbNailDefault = v.snippet.thumbnails.default.url;
// this.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
// this.urlThumbNailHigh = v.snippet.thumbnails.high.url;
// this.widthDefault = v.snippet.thumbnails.default.width.toString();
// this.heightDefault = v.snippet.thumbnails.default.height.toString();
// this.widthMedium = v.snippet.thumbnails.medium.width.toString();
// this.heightMedium = v.snippet.thumbnails.medium.height.toString();
// this.widthHigh = v.snippet.thumbnails.high.width.toString();
// this.heightHigh = v.snippet.thumbnails.high.height.toString();
// this.seqNumber = _seqNum;
// }
// this.seqNumber = 0;
}
}
Then, we can define the rxResource
with a property called loader
which makes the API call.
Inside the loader, we use expand
which is used for recursive API calls, we also use takeUntil
to stop the sequence, when there is no next API token.
Finally, we merge the results using reduce
and construct the final result.
youtube = rxResource({
loader: () => {
let _seqNum = 0;
let records: any = [];
return this.getUrlYouTube().pipe(
expand((response: any) =>
this.getNextVideoPage(response['nextPageToken'])
),
takeWhile((response: any) => !!response['nextPageToken'], true),
reduce((all: any, data: any) => all.concat(data.items), []),
map((response: any) => {
console.log(response);
return this.parseVideoList(response, _seqNum);
})
);
},
});
import { Component } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import { of, first, EMPTY, expand, takeWhile, tap, reduce, map } from 'rxjs';
import { CommonModule } from '@angular/common';
export class WebtubeSeries {
videoTitle: any;
urlThumbNailDefault: any;
urlThumbNailMedium: any;
videoId: any;
urlThumbNailHigh: any;
widthDefault: any;
heightDefault: any;
widthMedium: any;
heightMedium: any;
widthHigh: any;
heightHigh: any;
seqNumber: number;
constructor(v: any, _seqNum = 0) {
this.seqNumber = _seqNum;
// this.videoTitle = v.snippet.title;
// this.videoId = v.snippet.resourceId.videoId;
// if (v.snippet.thumbnails) {
// this.urlThumbNailDefault = v.snippet.thumbnails.default.url;
// this.urlThumbNailMedium = v.snippet.thumbnails.medium.url;
// this.urlThumbNailHigh = v.snippet.thumbnails.high.url;
// this.widthDefault = v.snippet.thumbnails.default.width.toString();
// this.heightDefault = v.snippet.thumbnails.default.height.toString();
// this.widthMedium = v.snippet.thumbnails.medium.width.toString();
// this.heightMedium = v.snippet.thumbnails.medium.height.toString();
// this.widthHigh = v.snippet.thumbnails.high.width.toString();
// this.heightHigh = v.snippet.thumbnails.high.height.toString();
// this.seqNumber = _seqNum;
// }
// this.seqNumber = 0;
}
}
@Component({
selector: 'app-root',
imports: [CommonModule],
template: `
{{youtube.value() | json}}
`,
})
export class App {
a = 0;
record: any = {
webTubeSeries: [],
};
getUrlYouTube() {
this.a++;
const randon = Math.random();
return of({
items: [0, 0, 0, 0, 0],
nextPageToken: randon,
});
}
youtube = rxResource({
loader: () => {
let _seqNum = 0;
let records: any = [];
return this.getUrlYouTube().pipe(
expand((response: any) =>
this.getNextVideoPage(response['nextPageToken'])
),
takeWhile((response: any) => !!response['nextPageToken'], true),
reduce((all: any, data: any) => all.concat(data.items), []),
map((response: any) => {
console.log(response);
return this.parseVideoList(response, _seqNum);
})
);
},
});
private parseVideoList(response: any, _seqNum: number) {
const result = [];
for (let v of response) {
console.log(v);
_seqNum++;
// if (v.snippet.thumbnails) {
result.push(new WebtubeSeries(v, _seqNum));
// }
}
return result;
}
private getNextVideoPage(_token: string) {
this.a++;
const randon = Math.random();
return of({
items: [1, 2, 3, 4, 5],
nextPageToken: this.a > 10 ? null : randon,
});
// let url = this.urlYouTube + this.videoListId + '&pageToken=' + _token;
// return this.http.get(url);
}
}
bootstrapApplication(App);
Upvotes: 0