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:
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"]
});
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!
With help from #stvar in the original question's comments, I was able to achieve this. The flow is as such:
Enable the YouTube Data API v3 from the Google Developers Console via the Enable APIs and Services.
Create a new OAuth client ID under YouTube Data API v3's "Credentials" pane and select Desktop app.
Save the client_id and client_secret. Make these accessible to your Node app via whatever environment variable method you prefer.
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);
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);
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.
Related
I'm trying to perform authentication on GoogleAPIs using a Service Account. I have a service account set up, with its credentials located at credentials.json. I try to access a private sheet, to which I added the E-Mail address of the service account with editing rights.
Here the code I am using:
const {
google
} = require('googleapis');
const fs = require('fs');
let scopes = ['https://www.googleapis.com/auth/spreadsheets'];
let credentials = require("./credentials.json");
const authClient = new google.auth.JWT(
credentials.client_email,
null,
credentials.private_key,
scopes);
authClient.authorize(function(err, tokens) {
if (err) {
console.log(err);
return;
} else {
authClient.setCredentials(tokens);
}
});
const sheets = google.sheets({
version: 'v4',
authClient
});
let spreadsheetId = //...
let range = //...
const request = {
spreadsheetId: spreadsheetId,
range: range
};
sheets.spreadsheets.values.get(request, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
} else {
console.log('Result: ' + response);
}
});
I guess the API changed over time, since many guides showed different approaches, and in the end none worked for me.
The error is as follows:
The API returned an error: Error: The request is missing a valid API key.
To my understanding, a simple API key should only be necessary for unauthenticated access on public sheets, so I don't get why it is even requiring that. If I add such an API key I get the error
The API returned an error: Error: The caller does not have permission
Using
$ npm list googleapis
`-- googleapis#52.1.0
Any help would be greatly appreciated.
For who still facing googleapis problems within NodeJS Runtime in 2022.
Firstly, redirect into Google-IAM-Admin/ServiceAccount to pick the current working project.
Secondly, click to jump into Service Account that has the following format project#sub-name-id.iam.gserviceaccount.com.
Thirdly, between [Details, Permissions, Keys, Metrics, Logs]. Jump into Keys then Add Key -> Create new Key -> Key type::JSON and save JSON file to your computer.
Here within NodeJS Runtime, I use the following Semantic Version
googleapis#100.0.0
You can create JWT Client and inject into google default auth at google.options({auth: client}); or provide auth-client to specific Service as google.chat({version: 'v1', auth: client});
However, in the following example. I create a GoogleAuth instance and then make an AuthClient after. Which resulted the same behaviour to the JWT Method.
/** Import Node Native Dependencies !*/
import * as path from "path";
/** Import ES6 Default Dependencies !*/
import {google} from "googleapis";
const {client_email, private_key} = require('$/keys/credentials.json');
/**
** #description - Google [[Service Account]] Authenticator.
**/
const auth = new google.auth.GoogleAuth({
keyFile: path.resolve('keys/credentials.json'),
/** Scopes can be specified either as an array or as a single, space-delimited string; ~!*/
scopes: [
"https://www.googleapis.com/auth/chat.bot",
],
});
const client = new google.auth.JWT({
email: client_email,
key: private_key,
/** Scopes can be specified either as an array or as a single, space-delimited string; ~!*/
scopes: [
"https://www.googleapis.com/auth/chat.bot",
],
});
(async () => {
/** #description - Either [[Get Client]] from [Google Auth] or Use directly from [JWT Client] ~!*/
const client = await auth.getClient();
/** #description - Use this Authorized Client as Default Authenticated to fallback from [Non-Authenticated Services] ~!*/
google.options({auth: client});
const chat = google.chat({
version: 'v1',
/** #description - Provide [Authenticated Services] to [Google Chat Service] Instance ~!*/
auth: client,
});
const response = await chat.spaces.members.get({
// Required. Resource name of the attachment, in the form "spaces/x/messages/x/attachments/x".
name: 'spaces',
});
console.log('response', response.data);
return void 0;
})();
Im using AdonisJs as my Node Framework, the xeroclient config works perfectly, I can extract details that I need.
I just cant disconnect, as I saw in their sample app we just have to call xero.disconnect, yet i am receiving an error xero.disconnect is not a function
im using the xero-node npm package
const xeroNode = require('xero-node')
const xero = new xeroNode.XeroClient({
clientId: Config.get('xero.client_id'),
clientSecret: Config.get('xero.client_secret'),
redirectUris: [Config.get('xero.redirectUri')],
scopes: Config.get('xero.scopes').split(" ")
})
async disconnect ({ response, session }) {
...
await xero.disconnect(xero.tenantIds[0])
...
}
Only thing that looks incorrect is the parameter you are passing to the function. That should actually be the connection object's ID not the tenantId itself: await xero.disconnect(xero.tenants[0].id)
But other than that can you elaborate the package version you are using? I've just mimicked everything about how you are importing and setting up client, so it's unclear why the disconnect function is not available. Please ensure you are using the most recent version 4.6.0 - the following just worked for me after I successfully got my access token back from the /callback flow.
Setup:
const xeroNode = require('xero-node')
const xero = new xeroNode.XeroClient({
clientId: client_id,
clientSecret: client_secret,
redirectUris: [redirectUrl],
scopes: scopes.split(' '),
});
/connect
const consentUrl: = await xero.buildConsentUrl();
/callback
const tokenSet = await xero.apiCallback(returning_url);
await xero.updateTenants();
/disconnect
const connection = xero.tenants[0]
await xero.disconnect(connection.id)
If you can post/log some more info can get this sorted for you!
I'm using Service Account's to manipulate my own spreadsheet, I know my script is working because I've tried it direct by Node (running node myScript.js)
My script is like this:
const { GoogleSpreadsheet } = require('google-spreadsheet');
async function start() {
const creds = require('./creds-from-google.json');
const sheetID = 'my spredsheets URL id here';
const doc = new GoogleSpreadsheet(sheetID);
await doc.useServiceAccountAuth({
client_email: creds.client_email,
private_key: creds.private_key,
});
await doc.loadInfo(); // loads document properties and worksheets
console.log(doc.title);
const sheet = doc.sheetsByIndex[0];
console.log(sheet.title);
const data = {
'Title': 'my title here',
'Content': 'my content here'
};
return sheet.addRow(data);
}
start();
But when I execute this code by Telegram bot, it's getting 401 error.
Error message:
Google API error - [401] Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.
Trying to make my expressJS app control users' google drive files. I've been utilizing the node 'googleapis' package. After following a slightly dated/incorrect article here, I've gotten the script to:
redirect a user to their authorization url
grab the 'code' from get parameter and then...
register it back as access tokens, which can then be used to...
create a registered 'auth' object
When I use this to create the drive object and try to get it to list files, I get the following error: 'Error: Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup'
This error has already popped up on SO question, and on github.
Following general advice, I re-enabled the drive api, re-downloaded access key. I've also tried replacing the sensitive drive scope a gmail scope, but that didn't work either. I'm not sure where else turn to start debugging at this point. I have a sneaking suspicion my entire auth object is being formed incorrectly but I can't find anything wrong.
This is the related piece of Express app code I'm using to create the authObject and then read drive files.
/**
* Google Utility class that packages different actions related to authentication
*/
class GoogleUtil {
constructor(secretFileName = 'client_secret.json') {
const secret = JSON.parse(fs.readFileSync(secretFileName)).web;
const { client_id, client_secret, redirect_uris } = secret;
this.client_id = client_id;
this.client_secret = client_secret;
this.redirect_uri = redirect_uris[0];
this.standardScope = [
'https://www.googleapis.com/auth/drive',
// 'https://www.googleapis.com/auth/gmail.readonly',
// 'https://www.googleapis.com/auth/userinfo.profile'
];
}
createConnection() {
return new google.auth.OAuth2(this.client_id, this.client_secret, this.redirect_uri); // form authObject
}
getConnectionUrl(auth) {
return auth.generateAuthUrl({
access_type: 'offline',
prompt: 'consent',
scope: this.standardScope
});
}
async getAccessTokenFromCode(code) {
var auth = this.createConnection();
const data = await auth.getToken(code);
return data.tokens;
}
}
const g = new GoogleUtil();
/**
* BEGIN ROUTE DEFINITIONS
*/
// THIS IS FIRST STEP. FRONTEND WILL REDIRECT TO GIVEN URL
app.get('/api/googleLoginUrl', async (req, res) => {
const oAuth2Client = g.createConnection();
const url = g.getConnectionUrl(oAuth2Client);
res.json({ url });
});
// *****
// NOTE: THIS IS ROUTE THAT ATTEMPTS TO LIST FILES AND THROWS ERROR
// *****
app.get('/google-auth-redirect', async (req, res) => {
if (!req.query.code) return res.send('Malformed request');
const tokens = await g.getAccessTokenFromCode(req.query.code);
const auth = g.createConnection().setCredentials(tokens);
const drive = google.drive({ version: 'v3', auth: auth });
drive.files.list({
pageSize: 10,
fields: 'nextPageToken, files(id, name)',
}, (err, resp) => {
if (err) return console.log('The API returned an error: ' + err);
console.log(resp);
});
res.redirect('/');
});
In the google developer console, clicking on 'create credentials' in the drive API overview informs me that my current credentials are compatible. The project scopes do include ' ../auth/drive'.
I'd want it to be able to list files from an authenticated user's account.
I think this might be related to how you are asking for permissions. If you are using your application to manipulate user's drive files you need a couple of things:
Check you have the correct access scopes setup.
Check you authentication parameter/Oauth screen is setup with said scopes.
You might want to read some documentation regarding authorizing users
Hope this helps! ;)
To facilitate updating of content on our information screen, we are looking at putting the content on Google Drive and then allowing the content to be synced via a NodeJS based application.
At the moment I am trying to test this approach using folder shared from my own account.
What I have so far, based on the documentation at https://github.com/google/google-api-nodejs-client/tree/master :
var google = require('googleapis');
var OAuth2 = google.auth.OAuth2;
var readline = require('readline');
const CLIENT_ID = 'xxxxxxxx.apps.googleusercontent.com';
const CLIENT_SECRET = '7h3c13n7s3cr37';
const REDIRECT_URL = 'https://accounts.google.com/o/oauth2/auth';
var oauth2Client = new OAuth2(
CLIENT_ID, CLIENT_SECRET, REDIRECT_URL);
var scopes = [
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/drive.metadata.readonly'
];
var url = oauth2Client.generateAuthUrl({
access_type: 'offline', // 'online' (default) or 'offline' (gets refresh_token)
scope: scopes // If you only need one scope you can pass it as string
});
console.log('past following URL into a web browser');
console.log(url);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Provide Key generated on web page ', (answer) => {
// store response key in file?
var drive = google.drive({ version: 'v2', auth: oauth2Client });
var folderId = 'mif01d3r';
drive.children.list({
auth: answer,
folderId: folderId,
}, function(error, response) {
if (error) {
console.log('err: ', error);
return;
}
console.log(response);
});
});
The current issue here is that the value for 'REDIRECT_URL' does not seem suitable. What should I be putting here for a command line application?
There is a much easier way. Manually generate a refresh token and then reference that (securely of course) from your app. In this way there is no auth required, no redirect URL's, etc etc.
See How do I authorise an app (web or installed) without user intervention? (canonical ?)
https://developers.google.com/api-client-library/python/auth/installed-app#choosingredirecturi is more for Python, but the below is applicable to all "Installed Applications"
urn:ietf:wg:oauth:2.0:oob
This value signals to the Google Authorization Server that the authorization code should be returned in the title bar of the browser, with the page text prompting the user to copy the code and paste it in the application. This is useful when the client (such as a Windows application) cannot listen on an HTTP port without significant client configuration.
See also:
https://developers.google.com/identity/protocols/OAuth2InstalledApp#choosingredirecturi
so it should be:
const REDIRECT_URL = 'urn:ietf:wg:oauth:2.0:oob';