1

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!

John D.
  • 2,521
  • 3
  • 24
  • 45
  • 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](https://developers.google.com/youtube/v3/quickstart/nodejs))? 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 Mar 05 '21 at 06:59
  • @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. Mar 05 '21 at 18:13
  • 2
    That's very much possible: [YouTube Data API v3: video upload from server without opening the browser](https://stackoverflow.com/a/61855306/8327971). – stvar Mar 05 '21 at 18:17
  • 1
    @stvar excellent. Once I get that to work, I'll describe the steps necessary in an answer on this question. – John D. Mar 05 '21 at 18:20
  • 2
    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](https://stackoverflow.com/a/66476673/8327971). – stvar Mar 05 '21 at 18:20
  • 1
    Uggh, that's not good. Is there not a way to permanently grant my script access? I have full ownership of everything (youtube channel, Google Cloud Platform admin access). – John D. Mar 05 '21 at 18:35
  • 1
    Avoiding to deal with expiring refresh tokens -- that is to make those refresh tokens long-lived -- can only be achieved (by new, fresh Google rules) upon your app being verified and approved by Google. Interesting enough, older Google projects (created before Google imposing this new restriction) have *Publishing status* set automagically to *in production* (thus not needing verification at all). Also note that the verification is not needed for certain scopes: the Developers Console UI will indicate you whether the scopes you've chosen for your project imply or not verification. – stvar Mar 05 '21 at 18:44

1 Answers1

1

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.

John D.
  • 2,521
  • 3
  • 24
  • 45
  • Were you never able to figure how to grant your script permanent access to the youtube api, without you needing to manually intervene? – 55 Cancri Aug 27 '22 at 17:06
  • @55Cancri Nope. This project didn't last much longer than when I posted the question. Don't recall ever finding a way. – John D. Aug 29 '22 at 17:47