Retrieve videos using YouTube Data API v3 in Node.js

Photo by Jigar Panchal / Unsplash
Photo by Jigar Panchal / Unsplash

The YouTube Data API allows integrations with external applications to perform tasks on YouTube, such as:

  • Retrieve and edit a YouTube channel information
  • Upload a video in a playlist.
  • Retrieve, update, or delete videos
  • Post a comment in a video
  • And many more...

Google provides an SDK for programming languages such as JavaScript, Python, Java, PHP, Ruby, etc...

Access to the API requires a YouTube API key, which we saw how to generate in the blog post below.

Retrieve YouTube API v3 key with Node.js
In this post, we will see how to generate the Access token required to interact with the TouTube Data API v3 and use the OAuth client to retrieve information about a YouTube channel.

In this post, we will explore the capabilities of the YouTube Data API by learning how to:

  • Retrieve the videos of a playlist.
  • Filter the data to retrieve in a video or playlist.
  • Use pagination to retrieve all the videos of a playlist.
  • Select the fields to return for a channel, a playlist, or a video.

What we will do

As a YouTube channel for this demo, we will use the Amigoscode channel. The channel ID is UC2KfmYEM4KCuA1ZurravgYw; I showed how to find it in the previous tutorial.

We will build a Node.js application that retrieves all the videos of the Amigoscode YouTube channel. For each video, we want to retrieve the following information:

  • The title and the description
  • The date of publication and the duration
  • The video thumbnail
  • The views count, the likes count, and the comment count.
  • The video privacy status

The TypeScript below describes the shape of a YouTube video to return


type Video = {
  id: string;
  title: string;
  description: string;
  publishedAt: string;
  duration: string;
  thumbnail: {
    url: string;
    width: number;
    height: number;
  } | null;
  kind: string;
  privacyStatus: string;
  statsViewable: boolean;
  viewCount: number;
  likeCount: number;
  commentCount: number;
};

Prerequisites

To follow this tutorial, make sure you have the following tools installed on your computer.

Set up the project

At this step, you must have the following keys:

  • The Google Cloud project client ID
  • The Google Cloud project client's secret
  • The access token to access the YouTube API.
  • The refresh token to request a new access token.

We will initialize a Node.js project with TypeScript from the starter project we built on this tutorial. Let's clone it, install the dependencies, and add the dotenv package to manage the environment variables.


git clone https://github.com/tericcabrel/node-ts-starter.git node-youtube-api-data

cd node-youtube-api-data

yarn add dotenv

Create a file .env and add the three following keys inside:

  • GOOGLE_CLIENT_ID for the client ID.
  • GOOGLE_CLIENT_SECRET for the client secret.
  • CLIENT_TOKEN for the access token
  • CLIENT_REFRESH_TOKEN for the refresh token
  • CLIENT_EXPIRATION_DATE for the access token expiration date
  • YOUTUBE_CHANNEL_ID for the YouTube channel ID to retrieve videos from

The .env file looks like this:


GOOGLE_CLIENT_ID=<your_app_client_id>
GOOGLE_CLIENT_SECRET=<your_app_client_secret>
CLIENT_TOKEN=<your_youtube_api_access_token>
CLIENT_REFRESH_TOKEN=<your_youtube_api_refresh_token>
CLIENT_EXPIRATION_DATE=<your_youtube_api_client_expiration_date>
YOUTUBE_CHANNEL_ID=<youtube_channel_id>

Create the Google OAuth Client

Google provides a Node.js package named googleapis to interact with all their APIs and thus the YouTube Data API.

To consume a Google API, you must create an OAuth client to send requests; the Node.js package google-auth-library will be helpful because it is a client library for using OAuth 2.0 authorization and authentication with Google APIs.

Let's install these two Node.js packages:


yarn add googleapis google-auth-library

Open the file src/index.ts and add the code below:


import { configDotenv } from 'dotenv';
import { google } from 'googleapis';
import { Credentials } from 'google-auth-library';

configDotenv();

const { CLIENT_EXPIRATION_DATE, CLIENT_REFRESH_TOKEN, CLIENT_TOKEN, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, YOUTUBE_CHANNEL_ID } = process.env;

const { OAuth2 } = google.auth;
const SCOPE = 'https://www.googleapis.com/auth/youtube.readonly';

const oauth2Client = new OAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET);

if (!CLIENT_EXPIRATION_DATE) {
  throw new Error('The token expiration date is required');
}

if (!YOUTUBE_CHANNEL_ID) {
  throw new Error('The YouTube channel ID is required');
}

const credentials: Credentials = {
  access_token: CLIENT_TOKEN,
  expiry_date: +CLIENT_EXPIRATION_DATE, // The plus sign in front cast the string type to a number
  refresh_token: CLIENT_REFRESH_TOKEN,
  scope: SCOPE,
  token_type: 'Bearer',
};

oauth2Client.credentials = credentials;

const service = google.youtube('v3');

The code above reads the Google credentials from the environment variables and creates an instance of the OAuth client.

Retrieve videos in a YouTube playlist with the Google APIs SDK

To retrieve videos from the YouTube channel, we need the playlist ID to retrieve videos from, and that information is found in the channel information.

Retrieve the channel information

Create a file named src/utils.ts and add the code below:


import { OAuth2Client } from 'google-auth-library';
import { google } from 'googleapis';

type ChannelInfo = {
  id: string;
  subscriberCount: number;
  viewCount: number;
  videoCount: number;
  playlistId: string | null;
};

export const retrieveChannelInfo = (oauth2Client: OAuth2Client, channelId: string): Promise<ChannelInfo> => {
  const service = google.youtube('v3');

  return new Promise((resolve, reject) => {
    service.channels.list(
      {
        auth: oauth2Client,
        id: [channelId],
        part: ['contentDetails', 'statistics'],
      },
      (error, response) => {
        if (error) {
          return reject({ error, message: 'The API returned an error: ' });
        }

        if (!response) {
          return reject({ message: 'Response has no content!' });
        }

        const channels = response.data.items ?? [];

        if (channels.length === 0) {
          return reject({ message: 'No channel found.' });
        }

        const [channel] = channels;

        resolve({
          id: channel.id ?? '',
          playlistId: channel.contentDetails?.relatedPlaylists?.uploads ?? null,
          subscriberCount: +(channel.statistics?.subscriberCount ?? 0),
          videoCount: +(channel.statistics?.videoCount ?? 0),
          viewCount: +(channel.statistics?.viewCount ?? 0),
        });
      },
    );
  });
};

The above code calls the function channels.list() with two arguments, where the first one is an object containing the parameters of the request:

  • The id property receives an array of channel IDs to retrieve information from.
  • The part property is interesting because it defines the groups of data we want to retrieve. Because we want channel statistics, we added the part statictics; because we want the channel's playlist ID, we added the part contentDetails. The other available part is snippet.

Update the file src/index.ts and add the code below:


const main = async () => {
  try {
    const channel = await retrieveChannelInfo(oauth2Client, YOUTUBE_CHANNEL_ID);

    console.log(channel);
  } catch (error) {
    console.error(error);
  }
};

void main();

Run the command yarn start to execute the file, we get the following output:

Retrieve the YouTube channel information.
Retrieve the YouTube channel information.

From the above screenshot, the channel playlist ID is UU2KfmYEM4KCuA1ZurravgYw.

Retrieve videos in the playlist

The YouTube data API v3 SDK provides the function playlistItems() to retrieve videos in a playlist. The available parts are contentDetails , snippet, status. The will looks like this:


service.playlistItems.list({
  playlistId: 
  part: ['contentDetails', 'snippet'],
});

This function doesn't return the statistics about a video, such as views count, comments count and likes count, but another function of the SDK videos.list() provides this information but takes as a parameter of video IDs. You can't just give a playlist ID; they get all the information.

So we will use the playlistItems.list() to retrieve all the video IDs and use the function videos.list() to retrieve the videos with all the information.

Read the playlistItems.list() to learn more about the parameters you can use.

Select the fields to retrieve from the SDK

We saw that the parts allow retrieving a data group, but we can go further and select only some fields inside the data group.

This is useful because we only want the video ID from playlistItems.list() function, and it reduces the network latency because you download data.

To select fields to retrieve, you must pass the property fields in the parameters; the value follows a specific format you can learn more about here.

Handle pagination when retrieving the data

A call to the YouTube Data API returns a maximum of 50 items with a token to retrieve the following 50 items. There is no data to retrieve when the token is null.

To retrieve all the 400+ videos of the Amigoscode channel, we will do a loop to retrieve those 50 video items and push them into an array.

Update the file, src/utils.ts to add these two functions below that retrieve the video ID from the channel using the playlist's ID.


const retrievePlaylistVideos = (oauth2Client: OAuth2Client, playlistId: string, nextPageToken: string | null | undefined): Promise<VideoResult> => {
  const service = google.youtube('v3');

  return new Promise((resolve, reject) => {
    service.playlistItems.list(
      {
        auth: oauth2Client,
        maxResults: 50,
        pageToken: nextPageToken ?? undefined,
        part: ['contentDetails'],
        playlistId,
        fields: 'items(id,contentDetails(videoId))',
      },
      (error, response) => {
        if (error) {
          return reject({ error, message: 'The API returned an error: ' });
        }

        if (!response) {
          return reject({ message: 'Response has no content!' });
        }

        const videos = response.data.items ?? [];

        if (videos.length === 0) {
          return reject({ message: 'No video from the playlist found.' });
        }

        const formattedVideos = videos.map((video): VideoFromPlaylistInfo => {
          return {
            id: video.id ?? '',
            videoId: video.contentDetails?.videoId ?? '',
          };
        });

        resolve({
          items: formattedVideos,
          nextPageToken: response.data.nextPageToken,
        });
      },
    );
  });
};

export const recursivelyRetrievePlaylistVideos = async (
  oauth2Client: OAuth2Client,
  playlistId: string,
  nextPageToken: string | null | undefined,
): Promise<VideoFromPlaylistInfo[]> => {
  const videosList: VideoFromPlaylistInfo[] = [];
  let token: string | undefined | null = nextPageToken;

  do {
    const result = await retrievePlaylistVideos(oauth2Client, playlistId, token);

    videosList.push(...result.items);

    token = result.nextPageToken ?? null;
  } while (token !== null);

  return videosList;
};

Update the file src/index.ts to use the function.


const main = async () => {
  try {
    const channel = await retrieveChannelInfo(oauth2Client, YOUTUBE_CHANNEL_ID);

    console.log(channel);

    if (!channel.playlistId) {
      console.error('The channel has no playlist');
      process.exit(1);
    }

    console.log(`Retrieve videos in the playlist: ${channel.playlistId}`);

    const videos = await recursivelyRetrievePlaylistVideos(oauth2Client, channel.playlistId, undefined);

    console.log(`Successfully retrieved ${videos.length} videos!`);

    const videoIds = videos.map((video) => video.videoId);

    console.log(videoIds);
  } catch (error) {
    console.error(error);
  }
};

void main();

Re-run the command yarn start to execute the file, we get the following output:

We can retrieve all the video data now that we have the video IDs. Update the file src/utils.ts to add the code below that retrieves all the YouTube channel videos from the video IDs.



type Video = {
  id: string;
  title: string;
  description: string;
  publishedAt: string;
  duration: string;
  thumbnail: {
    url: string;
    width: number;
    height: number;
  } | null;
  kind: string;
  privacyStatus: string;
  statsViewable: boolean;
  viewCount: number;
  likeCount: number;
  commentCount: number;
};

const chunk = (videos: string[], count: number): string[][] => {
  const chunks: string[][] = [];
  let i = 0;
  const n = videos.length;

  while (i < n) {
    chunks.push(videos.slice(i, (i += count)));
  }

  return chunks;
};

const retrieveVideosWithMetadata = (oauth2Client: OAuth2Client, videoIds: string[]): Promise<Video[]> => {
  const service = google.youtube('v3');

  return new Promise((resolve, reject) => {
    service.videos.list(
      {
        auth: oauth2Client,
        id: videoIds,
        part: ['snippet', 'contentDetails', 'status', 'statistics'],
      },
      (error, response) => {
        if (error) {
          return reject({ error, message: 'The API returned an error: ' });
        }

        if (!response) {
          return reject({ message: 'Response has no content!' });
        }

        const videos = response.data.items ?? [];

        if (videos.length === 0) {
          return reject({ message: 'No video found.' });
        }

        const formattedVideos = videos.map((video): Video => {
          const thumbnail = video.snippet?.thumbnails?.high;

          return {
            commentCount: +(video.statistics?.commentCount ?? 0),
            description: video.snippet?.description ?? '',
            duration: video.contentDetails?.duration ?? '',
            id: video.id ?? '',
            kind: video.kind ?? '',
            likeCount: +(video.statistics?.likeCount ?? 0),
            privacyStatus: video.status?.privacyStatus ?? '',
            publishedAt: video.snippet?.publishedAt ?? '',
            statsViewable: video.status?.publicStatsViewable ?? false,
            thumbnail: {
              url: thumbnail?.url ?? '',
              width: thumbnail?.width ?? 0,
              height: thumbnail?.height ?? 0,
            },
            title: video.snippet?.title ?? '',
            viewCount: +(video.statistics?.viewCount ?? ''),
          };
        });

        resolve(formattedVideos);
      },
    );
  });
};

export const retrieveAllVideosWithMetadata = async (oauth2Client: OAuth2Client, videoIds: string[]): Promise<Video[]> => {
  const videosList: Video[] = [];

  const videoIdChunks = chunk(videoIds, 50);

  for (const videoIds of videoIdChunks) {
    const result = await retrieveVideosWithMetadata(oauth2Client, videoIds);

    videosList.push(...result);
  }

  return videosList;
};

This is the final content of the src/index.ts


import fs from 'fs';
import { configDotenv } from 'dotenv';
import { google } from 'googleapis';
import { Credentials } from 'google-auth-library';
import { recursivelyRetrievePlaylistVideos, retrieveAllVideosWithMetadata, retrieveChannelInfo } from './utils';

configDotenv();

const { CLIENT_EXPIRATION_DATE, CLIENT_REFRESH_TOKEN, CLIENT_TOKEN, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, YOUTUBE_CHANNEL_ID } = process.env;

const { OAuth2 } = google.auth;
const SCOPE = 'https://www.googleapis.com/auth/youtube.readonly';

const oauth2Client = new OAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET);

if (!CLIENT_EXPIRATION_DATE) {
  throw new Error('The token expiration date is required');
}

if (!YOUTUBE_CHANNEL_ID) {
  throw new Error('The YouTube channel ID is required');
}

const credentials: Credentials = {
  access_token: CLIENT_TOKEN,
  expiry_date: +CLIENT_EXPIRATION_DATE, // The plus sign in front cast the string type to a number
  refresh_token: CLIENT_REFRESH_TOKEN,
  scope: SCOPE,
  token_type: 'Bearer',
};

oauth2Client.credentials = credentials;

const main = async () => {
  try {
    const channel = await retrieveChannelInfo(oauth2Client, YOUTUBE_CHANNEL_ID);

    console.log(channel);

    if (!channel.playlistId) {
      console.error('The channel has no playlist');
      process.exit(1);
    }

    console.log(`Retrieve videos in the playlist: ${channel.playlistId}`);

    const videos = await recursivelyRetrievePlaylistVideos(oauth2Client, channel.playlistId, undefined);

    console.log(`Successfully retrieved ${videos.length} videos!`);

    const videoIds = videos.map((video) => video.videoId);

    console.log('Retrieve videos with metadata...');

    const videosWithData = await retrieveAllVideosWithMetadata(oauth2Client, videoIds);

    console.log(`Successfully retrieved metadata of ${videosWithData.length} videos!`);

    const filePath = `./src/videos.json`;
    const content = JSON.stringify({ items: videosWithData }, null, 2);

    fs.writeFileSync(filePath, content, { encoding: 'utf-8' });

    console.log('Successfully exported the videos in the file');
  } catch (error) {
    console.error(error);
  }
};

void main();

After retrieving all the video data, we store them in a JSON file.

Re-run the command yarn start to execute the file, we get the following output:

0:00
/0:34

Demo of a script running to retrieve all the videos of a YouTube channel.

We successfully retrieved all the videos and saved them in the JSON file 🎉.

Wrap up

We saw how to retrieve all the YouTube channel videos from the YouTube Data API using the Google APIs SDK for Node.js, which makes the interaction easy. Consuming the YouTube API can be summarized into the following:

  • Get the YouTube Data API access token.
  • Retrieve the channel information and the playlist ID.
  • Retrieve all the videos in the playlist.
  • Use pagination and data filtering to efficiently retrieve data.

You can also use The YouTube Data API to perform actions that edit Your channel; this is great for building automation around YouTube.

Here are some resources to go further:

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.