Skip to content

Commit 9105f46

Browse files
committed
add yt-update script
1 parent 7ab1125 commit 9105f46

File tree

5 files changed

+4059
-2968
lines changed

5 files changed

+4059
-2968
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ public
44
.DS_Store
55
_descriptions
66
.netlify
7+
google-credentials

node-scripts/update-yt.mjs

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// npm run update-yt
2+
// Updates YouTube video descriptions according to description generated by from json data.
3+
4+
// ===========================================================
5+
// HOW TO SETUP GOOGLE OAuth2 CREDENTIALS
6+
// ===========================================================
7+
// 1. Go to https://console.developers.google.com/
8+
// 2. Click on the dropdown on the top left. Create a new project / select an existing project.
9+
// 3. Enable YouTube Data API v3 in this project by going to APIs & Services > Enable APIs and Services > Search for 'YouTube Data API v3' > Click Enable.
10+
// 4. Go to Credentials > Create Credentials > OAuth client ID. Here you might be asked to CONFIGURE CONSENT SCREEN. (If not, skip to step 5). Click on this button
11+
// and fill in the required fields. (User type: 'External', App name, User support email, Developer contact information as required)
12+
// Scopes: Add 'https://www.googleapis.com/auth/youtube'. On the summary page, scroll below to "Test Users" and add your email address. (This must be a Google account
13+
// which has write access to the Coding Train YouTube channel.)
14+
// 5. Go to Credentials > Create Credentials > OAuth client ID > Application type: Desktop app > Create. Here, click on "DOWNLOAD JSON" to download the credentials file.
15+
// Save this file as `google-credentials/client_secret.json` in this repo.
16+
// ===========================================================
17+
//
18+
//
19+
// ===========================================================
20+
// RUNNING THE SCRIPT
21+
// ===========================================================
22+
// 1. Run `npm run update-yt`
23+
// 2. If running the script for the first time, you will be asked to visit a URL to authenticate the app. Open this URL in your browser,
24+
// and login with the Google account which has write access to the Coding Train YouTube channel. You will be asked to grant permissions to the app.
25+
// After granting permissions, you will be redirected to a localhost page. Copy the `code` query param from the URL and paste it in the terminal.
26+
// This will store the auth token and a refresh token in `google-credentials/credentials.json`, which will be used for subsequent runs.
27+
// 3. For updating the description of a video, it is required to first generate the descriptions using the `yt-desc` script.
28+
// ===========================================================
29+
30+
import fs from 'fs';
31+
import { createInterface } from 'readline';
32+
import { google, youtube_v3 } from 'googleapis';
33+
import inquirer from 'inquirer';
34+
35+
const SCOPES = ['https://www.googleapis.com/auth/youtube'];
36+
const TOKEN_DIR = 'google-credentials/';
37+
const TOKEN_PATH = TOKEN_DIR + 'credentials.json';
38+
const OAuth2 = google.auth.OAuth2;
39+
40+
/**
41+
* Create an OAuth2 client with the given credentials.
42+
*
43+
* @param {Object} credentials The authorization client credentials.
44+
*/
45+
async function authorize(credentials) {
46+
const clientSecret = credentials.installed.client_secret;
47+
const clientId = credentials.installed.client_id;
48+
const redirectUrl = credentials.installed.redirect_uris[0];
49+
const oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl);
50+
51+
// Check if we have previously stored a token.
52+
try {
53+
const token = await fs.promises.readFile(TOKEN_PATH);
54+
oauth2Client.credentials = JSON.parse(token);
55+
} catch (err) {
56+
await getNewToken(oauth2Client);
57+
}
58+
59+
return oauth2Client;
60+
}
61+
62+
/**
63+
* Get and store new token after prompting for user authorization.
64+
*
65+
* @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
66+
*/
67+
async function getNewToken(oauth2Client) {
68+
const authUrl = oauth2Client.generateAuthUrl({
69+
access_type: 'offline',
70+
scope: SCOPES
71+
});
72+
console.log('Authorize this app by visiting this url: ', authUrl);
73+
const rl = createInterface({
74+
input: process.stdin,
75+
output: process.stdout
76+
});
77+
78+
return new Promise((res, rej) => {
79+
rl.question('Enter the code from that page here: ', function (code) {
80+
rl.close();
81+
oauth2Client.getToken(code, function (err, token) {
82+
if (err) {
83+
console.log('Error while trying to retrieve access token', err);
84+
return rej();
85+
}
86+
oauth2Client.credentials = token;
87+
storeToken(token, res);
88+
});
89+
});
90+
});
91+
}
92+
93+
function storeToken(token, callback) {
94+
try {
95+
fs.mkdirSync(TOKEN_DIR);
96+
} catch (err) {
97+
if (err.code != 'EEXIST') {
98+
throw err;
99+
}
100+
}
101+
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
102+
if (err) throw err;
103+
console.log('Token stored to ' + TOKEN_PATH);
104+
callback();
105+
});
106+
}
107+
108+
/**
109+
* Updates the description of a YouTube video.
110+
* @param {string} videoId youtube video id
111+
* @param {string} newDescription new description to update
112+
* @param {youtube_v3.Youtube} service youtube service
113+
*/
114+
async function updateYTDesc(videoId, newDescription, service) {
115+
// YouTube Data API v3:
116+
// videos.update
117+
// ⚠️ Quota impact: A call to this method has a quota cost of 50 units.
118+
119+
try {
120+
const res = await service.videos.list({
121+
part: ['snippet'],
122+
id: videoId
123+
});
124+
const video = res.data.items[0];
125+
126+
// diff old and new description
127+
const oldDescription = video.snippet.description;
128+
if (oldDescription === newDescription) {
129+
console.log('Description is already up to date.');
130+
return;
131+
}
132+
133+
const res2 = await service.videos.update({
134+
part: ['snippet'],
135+
requestBody: {
136+
id: videoId,
137+
snippet: {
138+
title: video.snippet.title,
139+
description: newDescription,
140+
categoryId: video.snippet.categoryId
141+
}
142+
}
143+
});
144+
145+
console.log('Updated video description.');
146+
} catch (err) {
147+
console.error('The API returned an error: ' + err);
148+
}
149+
}
150+
151+
// Load client secrets from a local file.
152+
async function main() {
153+
let content;
154+
try {
155+
content = await fs.promises.readFile(
156+
'google-credentials/client_secret.json'
157+
);
158+
} catch (err) {
159+
console.log('Error loading client secret file: ' + err);
160+
return;
161+
}
162+
const auth = await authorize(JSON.parse(content));
163+
164+
const service = google.youtube({
165+
version: 'v3',
166+
auth
167+
});
168+
169+
if (
170+
!fs.existsSync('_descriptions') ||
171+
fs.readdirSync('_descriptions').length === 0
172+
) {
173+
console.log(
174+
'No generated descriptions available. Try generating them first by using the yt-desc script.'
175+
);
176+
return;
177+
}
178+
179+
const videoIds = fs
180+
.readdirSync('_descriptions')
181+
.filter((f) => !f.endsWith('json'))
182+
.map((f) => f.split('.')[0].split('_').slice(1).join('_'));
183+
const metadata = JSON.parse(
184+
fs.readFileSync('_descriptions/metadata.json', 'utf8')
185+
);
186+
187+
const videos = metadata.videos.filter((x) => videoIds.includes(x.videoId));
188+
const tracks = metadata.tracks
189+
.map((track) => {
190+
track.videos = videos.filter(
191+
(video) => video.canonicalTrack === track.slug
192+
);
193+
return track;
194+
})
195+
.filter((track) => track.videos.length > 0);
196+
const challengeVideos = videos.filter((video) =>
197+
video.canonicalURL.startsWith('challenges')
198+
);
199+
if (challengeVideos.length > 0) {
200+
tracks.push({
201+
slug: 'challenges',
202+
title: 'Coding Challenges',
203+
videos: challengeVideos
204+
});
205+
}
206+
207+
const { trackSlug } = await inquirer.prompt([
208+
{
209+
type: 'list',
210+
name: 'trackSlug',
211+
message: 'Select a track to update:',
212+
choices: tracks.map((track) => ({
213+
name: track.title,
214+
value: track.slug
215+
}))
216+
}
217+
]);
218+
const track = tracks.find((x) => x.slug === trackSlug);
219+
const { videoId } = await inquirer.prompt([
220+
{
221+
type: 'list',
222+
name: 'videoId',
223+
message: 'Select a video to update:',
224+
choices: track.videos.map((video) => ({
225+
name: video.title + ' (' + video.videoId + ')',
226+
value: video.videoId
227+
}))
228+
}
229+
]);
230+
const video = track.videos.find((video) => video.videoId === videoId);
231+
232+
console.log(
233+
'Updating description for video...',
234+
video.title,
235+
`(${video.videoId})`
236+
);
237+
238+
let newDescription = fs.readFileSync(
239+
`_descriptions/${video.slug}_${video.videoId}.txt`,
240+
'utf8'
241+
);
242+
243+
updateYTDesc(video.videoId, newDescription, service);
244+
}
245+
246+
main();

node-scripts/yt-description.mjs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
// npm run yt-desc
55
// npm run yt-desc https://thecodingtrain.com/path/to/video/page
66
// npm run yt-desc https://youtube.com/watch?v=videoId
7-
// npm run yt-desc ./path/to/index.json
8-
// npm run yt-desc ./path/to/index.json -- -c # copy to clipboard
7+
// npm run yt-desc path/to/index.json # path starts with content/videos
8+
// npm run yt-desc path/to/index.json -- -c # copy to clipboard
99

1010
// Output files are saved to `./_descriptions` directory
1111

@@ -63,7 +63,6 @@ class Video {
6363
/**
6464
* Searches for `index.json` files in a given directory and returns an array of parsed files.
6565
* @param {string} dir Name of directory to search for files
66-
* @param {?any[]} arrayOfFiles Array to store the parsed JSON files
6766
* @returns {any[]}
6867
*/
6968
function findContentFilesRecursive(dir) {
@@ -661,5 +660,22 @@ const allTracks = [...mainTracks, ...sideTracks];
661660
} else {
662661
videos.forEach(writeDescription);
663662
}
663+
const metadata = {
664+
videos: videos.map((v) => ({
665+
title: v.data.title,
666+
videoId: v.data.videoId,
667+
slug: v.slug,
668+
canonicalTrack: v.canonicalTrack,
669+
canonicalURL: v.canonicalURL
670+
})),
671+
tracks: allTracks.map((t) => ({
672+
slug: t.trackName,
673+
title: t.data.title
674+
}))
675+
};
676+
fs.writeFileSync(
677+
'./_descriptions/metadata.json',
678+
JSON.stringify(metadata, null, 2)
679+
);
664680
console.log('\n✅ Wrote descriptions to ./_descriptions/');
665681
})();

0 commit comments

Comments
 (0)