← back to Goodquestion Ai
scripts/youtube-upload.cjs
120 lines
/**
* YouTube Video Upload Script
* Usage: node youtube-upload.cjs <video-path> [--title "Title"] [--description "Desc"] [--tags "tag1,tag2"]
*
* Requires: Run youtube-auth.cjs first to get a refresh token.
*/
const { google } = require('googleapis');
const fs = require('fs');
const path = require('path');
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
const CLIENT_ID = process.env.YOUTUBE_CLIENT_ID;
const CLIENT_SECRET = process.env.YOUTUBE_CLIENT_SECRET;
const REDIRECT_URI = process.env.YOUTUBE_REDIRECT_URI || 'http://localhost:3456/oauth/callback';
const TOKEN_PATH = path.join(__dirname, '..', '.youtube-token.json');
function parseArgs() {
const args = process.argv.slice(2);
const result = { videoPath: null, title: null, description: null, tags: [], privacy: 'public' };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--title' && args[i + 1]) {
result.title = args[++i];
} else if (args[i] === '--description' && args[i + 1]) {
result.description = args[++i];
} else if (args[i] === '--tags' && args[i + 1]) {
result.tags = args[++i].split(',').map(t => t.trim());
} else if (args[i] === '--privacy' && args[i + 1]) {
result.privacy = args[++i];
} else if (!args[i].startsWith('--')) {
result.videoPath = args[i];
}
}
return result;
}
async function uploadVideo() {
const opts = parseArgs();
if (!opts.videoPath) {
console.error('Usage: node youtube-upload.cjs <video-path> [--title "Title"] [--description "Desc"] [--tags "tag1,tag2"] [--privacy public|private|unlisted]');
process.exit(1);
}
if (!fs.existsSync(opts.videoPath)) {
console.error(`Video file not found: ${opts.videoPath}`);
process.exit(1);
}
if (!fs.existsSync(TOKEN_PATH)) {
console.error('No YouTube token found. Run youtube-auth.cjs first.');
process.exit(1);
}
// Load saved tokens
const tokens = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf-8'));
const oauth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI);
oauth2Client.setCredentials(tokens);
// Auto-refresh token if expired
oauth2Client.on('tokens', (newTokens) => {
const merged = { ...tokens, ...newTokens };
fs.writeFileSync(TOKEN_PATH, JSON.stringify(merged, null, 2));
console.log('Token refreshed and saved.');
});
const youtube = google.youtube({ version: 'v3', auth: oauth2Client });
const filename = path.basename(opts.videoPath, path.extname(opts.videoPath));
const title = opts.title || filename;
const description = opts.description || `Agent Abrams — ${title}\n\nhttps://goodquestion.ai`;
const fileSize = fs.statSync(opts.videoPath).size;
console.log(`\nUploading: ${opts.videoPath}`);
console.log(`Title: ${title}`);
console.log(`Size: ${(fileSize / 1024 / 1024).toFixed(1)} MB`);
console.log(`Privacy: ${opts.privacy}`);
console.log('');
try {
const res = await youtube.videos.insert({
part: ['snippet', 'status'],
requestBody: {
snippet: {
title,
description,
tags: opts.tags.length > 0 ? opts.tags : ['claude-code', 'agent-abrams', 'development', 'ai'],
categoryId: '28', // Science & Technology
},
status: {
privacyStatus: opts.privacy,
selfDeclaredMadeForKids: false,
},
},
media: {
body: fs.createReadStream(opts.videoPath),
},
}, {
onUploadProgress: (evt) => {
const pct = Math.round((evt.bytesRead / fileSize) * 100);
process.stdout.write(`\rUploading... ${pct}%`);
},
});
console.log('\n\nUpload complete!');
console.log(`Video ID: ${res.data.id}`);
console.log(`URL: https://www.youtube.com/watch?v=${res.data.id}`);
console.log(`Studio: https://studio.youtube.com/video/${res.data.id}/edit`);
return res.data.id;
} catch (err) {
console.error('\nUpload failed:', err.message);
if (err.errors) console.error(JSON.stringify(err.errors, null, 2));
process.exit(1);
}
}
uploadVideo();