skip to Main Content

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);
    }
  }
}

2

Answers


  1. Chosen as BEST ANSWER
    import { Component, inject } 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';
    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: `
        {{youtube.value() | json}}
      `,
    })
    export class App {
      a = 0;
      record: any = {
        webTubeSeries: [],
      };
      public readonly YOUTUBE_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // your own Youtube Key
      public 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);
      getUrlYouTube() {
        return this.http.get(this.YOUTUBE_URL + this.videoListId)
      }
      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:'+JSON.stringify(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) {
        let url = this.YOUTUBE_URL + this.videoListId + '&pageToken=' + _token;
        return this.http.get(url);
      }
    }
    
    bootstrapApplication(App,{
      providers:[
        provideHttpClient()
    ]
    });
    function provideAnimationsAsync(): import("@angular/core").Provider | import("@angular/core").EnvironmentProviders {
      throw new Error('Function not implemented.');
    }
    
    

    To test it, you need a key from Youtube. You can use my existing videolistId or you can use your own one. Thanks.


  2. 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);
          })
        );
      },
    });
    

    Full Code:

    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);
    

    Stackblitz Demo

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search