Files
Termix/scripts/publish-youtube.cjs
T

121 lines
3.4 KiB
JavaScript

const fs = require("fs");
const path = require("path");
function extractYoutubeId(notes) {
const match = notes.match(
/<!--\s*YOUTUBE\s*-->([\s\S]*?)<!--\s*\/YOUTUBE\s*-->/,
);
if (!match || !match[1].trim()) {
throw new Error(
"missing or empty <!-- YOUTUBE --> section in release notes",
);
}
return parseYoutubeId(match[1].trim());
}
function parseYoutubeId(raw) {
const value = raw.trim();
let m = value.match(/[?&]v=([A-Za-z0-9_-]+)/);
if (m) return m[1];
m = value.match(/youtu\.be\/([A-Za-z0-9_-]+)/);
if (m) return m[1];
m = value.match(/embed\/([A-Za-z0-9_-]+)/);
if (m) return m[1];
if (/^[A-Za-z0-9_-]+$/.test(value)) return value;
throw new Error(`could not parse a YouTube video id from "${value}"`);
}
async function getAccessToken({ clientId, clientSecret, refreshToken }) {
const res = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken,
grant_type: "refresh_token",
}),
});
if (!res.ok) {
throw new Error(
`token exchange failed (${res.status}): ${await res.text()}`,
);
}
const json = await res.json();
return json.access_token;
}
async function getVideoStatus(accessToken, videoId) {
const res = await fetch(
`https://www.googleapis.com/youtube/v3/videos?part=status&id=${videoId}`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
if (!res.ok) throw new Error(`videos.list failed (${res.status}): ${await res.text()}`);
const json = await res.json();
return json.items?.[0]?.status?.privacyStatus ?? null;
}
async function setVideoPublic(accessToken, videoId) {
const status = await getVideoStatus(accessToken, videoId);
if (status === "public") {
console.log(`Video ${videoId} is already public, skipping.`);
return;
}
const res = await fetch(
"https://www.googleapis.com/youtube/v3/videos?part=status",
{
method: "PUT",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id: videoId,
status: { privacyStatus: "public" },
}),
},
);
if (!res.ok) {
throw new Error(
`videos.update failed (${res.status}): ${await res.text()}`,
);
}
return res.json();
}
async function main() {
const clientId = process.env.YOUTUBE_CLIENT_ID;
const clientSecret = process.env.YOUTUBE_CLIENT_SECRET;
const refreshToken = process.env.YOUTUBE_REFRESH_TOKEN;
if (!clientId || !clientSecret || !refreshToken) {
throw new Error(
"YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET, and YOUTUBE_REFRESH_TOKEN are required",
);
}
const notesPath = path.resolve(
process.env.RELEASE_NOTES || "RELEASE_NOTES.md",
);
const notes = fs.readFileSync(notesPath, "utf8");
const videoId = extractYoutubeId(notes);
const accessToken = await getAccessToken({
clientId,
clientSecret,
refreshToken,
});
await setVideoPublic(accessToken, videoId);
console.log(`Set YouTube video ${videoId} to public.`);
}
module.exports = { extractYoutubeId, parseYoutubeId };
if (require.main === module) {
main().catch((err) => {
console.error(`publish-youtube: ${err.message}`);
process.exit(1);
});
}