Created
June 3, 2023 16:47
-
-
Save dipamsen/b208e28a0120ac5501fab023d6835b56 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Coding Train Video Linker: converts youtube links into Coding train website relative paths, on thecodingtrain.com | |
// (add file to node-scripts/) | |
import fs, { copyFileSync } from 'fs'; | |
import path from 'path'; | |
import { globSync } from 'glob'; | |
import clipboard from 'clipboardy'; | |
const videos = []; | |
/** | |
* Searches for `index.json` files in a given directory and returns an array of parsed files. | |
* @param {string} dir Name of directory to search for files | |
* @param {?any[]} arrayOfFiles Array to store the parsed JSON files | |
* @returns {any[]} | |
*/ | |
function findContentFilesRecursive(dir) { | |
const files = globSync(`${dir}/**/index.json`); | |
return files; | |
} | |
/** | |
* Parse a track file | |
* @param {string} track track's `index.json` file | |
*/ | |
function parseTrack(track) { | |
let trackName = path.dirname(track); | |
trackName = trackName.slice(trackName.lastIndexOf(path.sep) + 1); | |
const content = fs.readFileSync(`./${track}`, 'utf-8'); | |
const parsed = JSON.parse(content); | |
let trackFolder, videoList, videoDirs; | |
if (parsed.chapters) { | |
// Main Track | |
videoDirs = parsed.chapters | |
.map((chap) => | |
chap.videos.map((video) => video.split('/').slice(0, -1).join('/')) | |
) | |
.flat() | |
.filter((dir) => dir !== 'challenges'); | |
videoList = parsed.chapters.map((chap) => chap.videos).flat(); | |
} else { | |
// Side Track | |
videoDirs = parsed.videos | |
.map((video) => video.split('/').slice(0, -1).join('/')) | |
.filter((dir) => dir !== 'challenges'); | |
videoList = parsed.videos; | |
} | |
if (videoDirs.length == 0) { | |
// ignore tracks with only challenges | |
return null; | |
} else if (videoDirs.length == 1) { | |
trackFolder = videoDirs[0]; | |
} else { | |
// find max used directory | |
trackFolder = findMaxOccurences(videoDirs); | |
} | |
return { | |
trackName, | |
trackFolder, | |
videoList, | |
data: parsed | |
}; | |
} | |
/** | |
* Parses index.json and returns an array | |
* @param {string} file File to parse | |
*/ | |
function getVideoData(file) { | |
const videoList = []; | |
const content = fs.readFileSync(`./${file}`, 'utf-8'); | |
const video = JSON.parse(content); | |
const filePath = file.split(path.sep).slice(2); | |
const videoPath = filePath.slice(0, -1).join('/'); | |
// console.log('[Parsing File]:', filePath.join('/')); | |
let url, canonicalTrack; | |
if (filePath[0] === 'challenges') { | |
url = filePath.slice(0, 2).join('/'); | |
} else { | |
if (video.canonicalTrack) { | |
canonicalTrack = video.canonicalTrack; | |
url = ['tracks', video.canonicalTrack, videoPath].join('/'); | |
} else { | |
for (let track of allTracks) { | |
if (track.videoList.includes(videoPath)) { | |
canonicalTrack = track.trackName; | |
url = ['tracks', track.trackName, videoPath].join('/'); | |
break; | |
} | |
} | |
} | |
} | |
if (!url) { | |
console.log( | |
'⚠️ Warning: Could not find this video: ' + | |
videoPath + | |
' in any track or challenge!' | |
); | |
return []; | |
} | |
const slug = url.split('/').at(-1); | |
if (!url || url.length == 0) { | |
throw new Error( | |
'Something went wrong in parsing this file: ' + filePath.join('/') | |
); | |
} | |
if (video.parts && video.parts.length > 0) { | |
// Multipart Coding Challenge | |
// https://github.com/CodingTrain/thecodingtrain.com/issues/420#issuecomment-1218529904 | |
for (const part of video.parts) { | |
// copy all info from base object | |
const partInfo = JSON.parse(JSON.stringify(video)); | |
delete partInfo.parts; | |
// copy videoId, title, timestamps from parts | |
partInfo.videoId = part.videoId; | |
partInfo.timestamps = part.timestamps; | |
partInfo.challengeTitle = video.title; | |
partInfo.partTitle = part.title; | |
partInfo.title = video.title + ' - ' + part.title; | |
const videoData = { | |
pageURL: url, | |
data: partInfo, | |
filePath: file, | |
isMultipartChallenge: true, | |
canonicalTrack, | |
slug: slug | |
}; | |
videoList.push(videoData); | |
} | |
} else { | |
video.challengeTitle = video.title; | |
const videoData = { | |
pageURL: url, | |
data: video, | |
filePath: file, | |
canonicalTrack, | |
slug: slug | |
}; | |
videoList.push(videoData); | |
} | |
return videoList; | |
} | |
/** | |
* Creates and resets a temporary directory | |
* @param {string} dir Directory Name | |
*/ | |
function primeDirectory(dir) { | |
if (!fs.existsSync(dir)) fs.mkdirSync(dir); | |
fs.rmSync(dir, { recursive: true }, (err) => { | |
if (err) { | |
throw err; | |
} | |
}); | |
fs.mkdirSync(dir, (err) => { | |
if (err) { | |
throw err; | |
} | |
}); | |
} | |
const playlistWarnings = new Set(); | |
/** | |
* Retrieves YouTube video/playlist url from relative website path | |
* @param {string} url original relative url | |
* @returns {string} resolved url | |
*/ | |
function resolveCTLink(url) { | |
if (/https?:\/\/.*/.test(url)) return url; | |
const location = url.startsWith('/') ? url.substring(1, url.length) : url; | |
const urlchunks = location.split('/'); | |
if (!['challenges', 'tracks'].includes(urlchunks[0])) { | |
// not linking to video page | |
return `https://thecodingtrain.com/${location}`; | |
} | |
if (urlchunks[0] === 'tracks' && urlchunks.length === 2) { | |
// track page | |
// try to get playlist id from track's index.json | |
const track = allTracks.find((t) => t.trackName === urlchunks[1]); | |
if (track && track.data.playlistId) { | |
const playlistId = track.data.playlistId; | |
return `https://www.youtube.com/playlist?list=${playlistId}`; | |
} else { | |
if (!playlistWarnings.has(urlchunks[1])) { | |
console.warn( | |
'⚠️ Warning: YT Playlist not found for track:', | |
urlchunks[1] | |
); | |
playlistWarnings.add(urlchunks[1]); | |
} | |
return `https://thecodingtrain.com/${location}`; | |
} | |
} | |
let page; | |
try { | |
page = videos.find((vid) => vid.pageURL === location).data; | |
} catch (err) { | |
console.warn('⚠️ Warning: Could not resolve to YT video:', url); | |
return `https://thecodingtrain.com${url}`; | |
} | |
return `https://youtu.be/${page.videoId}`; | |
} | |
const unresolvedVideos = new Set(); | |
const unresolvedTracks = new Set(); | |
/** | |
* Retrieves Coding Train track/video url from youtube link | |
* @param {string} url original youtube url | |
* @returns {string} resolved url | |
*/ | |
function resolveYTLink(url) { | |
if (url.startsWith('/')) return url; | |
const U = new URL(url); | |
if (U.hostname !== 'www.youtube.com' && U.hostname !== 'youtu.be') return url; | |
if (U.hostname == 'www.youtube.com' && U.pathname.includes('playlist')) { | |
// playlist | |
const playlistId = U.searchParams.get('list'); | |
const track = allTracks.find((t) => t.data.playlistId === playlistId); | |
if (track) { | |
return `/tracks/${track.trackName}`; | |
} else { | |
unresolvedTracks.add(playlistId); | |
// console.warn('⚠️ Warning: Could not resolve to CT track:', url); | |
return url; | |
} | |
} else if (U.hostname == 'www.youtube.com' && U.pathname.includes('watch')) { | |
// video | |
if (U.searchParams.has('t')) { | |
// timestamped video | |
return url; | |
} else { | |
const videoId = U.searchParams.get('v'); //U.pathname.split('/')[2]; | |
const video = videos.find((vid) => vid.data.videoId === videoId); | |
if (video) { | |
return '/' + video.pageURL; | |
} else { | |
unresolvedVideos.add(videoId); | |
// console.warn('⚠️ Warning: Could not resolve to CT video:', url); | |
return url; | |
} | |
} | |
} else if (U.hostname == 'youtu.be') { | |
if (U.searchParams.has('t')) { | |
return url; | |
} else { | |
const videoId = U.pathname.split('/')[1]; | |
const video = videos.find((vid) => vid.data.videoId === videoId); | |
if (video) { | |
return '/' + video.pageURL; | |
} else { | |
unresolvedVideos.add(videoId); | |
// console.warn('⚠️ Warning: Could not resolve to CT video:', url); | |
return url; | |
} | |
} | |
} | |
return url; | |
} | |
/** | |
* Finds the most occuring item in an array | |
* @param {string[]} arr array of items | |
*/ | |
function findMaxOccurences(arr) { | |
const counts = {}; | |
for (const item of arr) { | |
if (counts[item]) { | |
counts[item]++; | |
} else { | |
counts[item] = 1; | |
} | |
} | |
const max = Object.keys(counts).reduce((a, b) => | |
counts[a] > counts[b] ? a : b | |
); | |
return max; | |
} | |
// know about tracks beforehand | |
const mainTracks = findContentFilesRecursive('content/tracks/main-tracks') | |
.map(parseTrack) | |
.filter((x) => x); | |
const sideTracks = findContentFilesRecursive('content/tracks/side-tracks') | |
.map(parseTrack) | |
.filter((x) => x); | |
const allTracks = [...mainTracks, ...sideTracks]; | |
(async () => { | |
console.log('Finding Unnecessary Videolinks'); | |
const directory = 'content/videos'; | |
const files = findContentFilesRecursive(directory); | |
for (const file of files) { | |
videos.push(...getVideoData(file)); | |
} | |
for (const video of videos) { | |
(video.data.groupLinks || []) | |
.map((x) => x.links) | |
.flat() | |
.forEach((link) => { | |
// console.log('Before:', link.url); | |
// console.log('After:', resolveYTLink(link.url)); | |
const resolved = resolveYTLink(link.url); | |
if (resolved !== link.url) { | |
console.log('Before:', link.url); | |
console.log('After:', resolved); | |
const jsonTxt = fs.readFileSync(video.filePath, 'utf-8'); | |
// if (jsonTxt.search(link.url) == -1) { | |
// console.warn( | |
// '⚠️ Warning: Could not find link in file:', | |
// video.filePath | |
// ); | |
// process.exit(1); | |
// } | |
const nn = jsonTxt.replace(link.url, resolved); | |
fs.writeFileSync(video.filePath, nn); | |
} | |
}); | |
} | |
// console.log(unresolvedTracks); | |
// console.log(unresolvedVideos); | |
fs.writeFileSync( | |
'unresolved-tracks.json', | |
JSON.stringify( | |
[...unresolvedTracks].map( | |
(x) => `https://www.youtube.com/playlist?list=${x}` | |
) | |
) | |
); | |
fs.writeFileSync( | |
'unresolved-videos.json', | |
JSON.stringify([...unresolvedVideos].map((x) => `https://youtu.be/${x}`)) | |
); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment