1
votes

I am writing a Node.js script which will run in Lambda to periodically request the list of every video (public, unlisted, or private) for one of my organization's channels via the YouTube Data v3 API. In order to do this, it appears there are two steps:

  1. Executing the channels/list call https://developers.google.com/youtube/v3/docs/channels/list to get the "Uploads" playlist.
const channelResult = await google.youtube('v3').channels.list({
    auth: youtubeApiKey,
    part: ['snippet', 'contentDetails'],
    id: ["my_channel_id"]
});
  1. Executing the playlistItems/list https://developers.google.com/youtube/v3/docs/playlistItems/list to see all the videos.
const videosResult = await google.youtube('v3').playlistItems.list({
    auth: youtubeApiKey,
    part: ['snippet', 'status'],
    playlistId: "my_uploads_playlsit_id"
});

This only ever executes as a script running the cloud; there is no user interface or browser component. This all appeared to work in my test channel when I set the lone video there to public. But if I set it to private, I get:

The playlist identified with the request's <code>playlistId</code> parameter cannot be found.

What do I have to do in order to still access the Uploads playlist of my channel, and show private videos? Thanks!

1
Your code is kind of ambiguous. Does auth: youtubeApiKey passes to your API calls an API key? Or, else, youtubeApiKey is a valid credentials object (like shown, for example by the sample code of Node.js Quickstart)? Do note that for accessing private data you have to run to successful completion an OAuth 2 authorization/authentication flow as shown by the sample code mentioned.stvar
@stvar great question- it originally was an API key, but then I realized that apparently I needed better auth to access private data. So I changed the value passed into auth: to be a an new google.auth.OAuth2 client which I eventually got to work on my local machine. But I'm wondering how I can get this to work on the server side when there is no browser/person running this and it's getting triggered by cron?John D.
@stvar excellent. Once I get that to work, I'll describe the steps necessary in an answer on this question.John D.
But you need to be aware of the following very recent requirement imposed by Google: Python OAuth after few days fails refreshing tokens with “invalid_grant” error.stvar

1 Answers

0
votes

With help from @stvar in the original question's comments, I was able to achieve this. The flow is as such:

  1. Enable the YouTube Data API v3 from the Google Developers Console via the Enable APIs and Services.
  2. Create a new OAuth client ID under YouTube Data API v3's "Credentials" pane and select Desktop app.
  3. Save the client_id and client_secret. Make these accessible to your Node app via whatever environment variable method you prefer.
  4. Create a separate script specifically for getting a refresh_token via YouTube Data API v3 OAuth
import { google } from 'googleapis';
import prompts from 'prompts';

console.log("about to execute oauth");

const yt_client_id = process.env.YOUTUBE_CLIENT_ID;
const yt_client_secret = process.env.YOUTUBE_CLIENT_SECRET;

const oauthClient = new google.auth.OAuth2({
  clientId: yt_client_id,
  clientSecret: yt_client_secret,
  redirectUri: 'http://localhost'
});

const authUrl = oauthClient.generateAuthUrl({
  access_type: 'offline', //gives you the refresh_token
  scope: 'https://www.googleapis.com/auth/youtube.readonly'
});

const codeUrl = await prompts({
  type: 'text',
  name: 'codeURl',
  message: `Please go to \n\n${authUrl}\n\nand paste in resulting localhost uri`
});

const decodedUrl = decodeURIComponent(codeUrl.codeURl);
const code = decodedUrl.split('?code=')[1].split("&scope=")[0];
const token = (await oauthClient.getToken(code)).tokens;
const yt_refresh_token = token.refresh_token;
console.log(`Please save this value into the YOUTUBE_REFRESH_TOKEN env variable for future runs: ${yt_refresh_token}`);

await prompts({
  type: 'text',
  name: 'blank',
  message: 'Hit enter to exit:'
});

process.exit(0);
  1. Save the refresh token in another environment variable, accessible to your main data-fetching script. Use it as such:

import { google } from 'googleapis';

console.log("Beginning youtubeIndexer. Checking for valid oauth.");

const yt_refresh_token = process.env.YOUTUBE_REFRESH_TOKEN;
const yt_client_id = process.env.YOUTUBE_CLIENT_ID;
const yt_client_secret = process.env.YOUTUBE_CLIENT_SECRET;
const yt_channel_id = process.env.YOUTUBE_CHANNEL_ID;

const oauthClient = new google.auth.OAuth2({
  clientId: yt_client_id,
  clientSecret: yt_client_secret,
  redirectUri: 'http://localhost'
});

oauthClient.setCredentials({
  refresh_token: yt_refresh_token
});

const youtube = google.youtube("v3");
const channelResult = await youtube.channels.list({
  auth: oauthClient,
  part: ['snippet', 'contentDetails'],
  id: [yt_channel_id]
});

let nextPageToken = undefined;
let videosFetched = 0;

do {
  const videosResult = await youtube.playlistItems.list({
    auth: oauthClient,
    maxResults: 50,
    pageToken: nextPageToken,
    part: ['snippet', 'status'],
    playlistId: channelResult.data.items[0].contentDetails.relatedPlaylists.uploads
  });

  videosFetched += videosResult.data.items.length;

  nextPageToken = videosResult.data.nextPageToken;

  videosResult.data.items.map((video, index) => {
    //process the files as you need to.
  });
} while (nextPageToken);
  1. This last .map() function, marked with the "process the files as you need to" comment will receive every video in the channel, whether it be public, unlisted, or private.

NOTE: I do not know yet how long a given refresh_token will last, but assume that you will regularly need to run the first script again and update the refresh_token used via the second script's environment variable.