Last active
October 22, 2023 23:02
-
-
Save dlh3/de843ea6346290d4056af2dda70c596b to your computer and use it in GitHub Desktop.
A YouTube end-card spider to discover all the videos CGP Grey created for his RPS interactive video
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
https://gist.githubusercontent.com/dlh3/de843ea6346290d4056af2dda70c596b/raw/4f9c5b81ff423a341862c90f9945622efe7f0a6f/Game%2520Tree |
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
// copy and run treeify first (if you want the tree view) | |
// https://github.com/notatestuser/treeify/blob/master/treeify.js | |
// 1. open https://www.youtube.com/ | |
// 2. run this in the browser's console | |
videos = {}; | |
played = []; | |
async function discoverVideos(id) { | |
// skip videos we've already encountered | |
if (played.includes(id)) return; | |
else played.push(id); | |
videos[id] = await fetch('/watch?v=' + id) | |
.then(resp => resp.text()) | |
.then(body => body.match(/"shortDescription":"([^"]*)"/)[1] | |
.split(/\\n/) | |
.filter(part => part.includes('/watch')) | |
.map(async card => { | |
const [text, cardId] = card.split("https://www.youtube.com/watch?v="); | |
console.log(text, cardId); | |
await discoverVideos(cardId); | |
return [text.replace(': ', ''), cardId]; | |
})) | |
.then(Promise.all.bind(Promise)) | |
.then(cards => cards.reduce((obj, [label, id]) => (obj[label] = id, obj), {})) | |
.catch(console.error); | |
} | |
function buildGameTree(id) { | |
return { | |
[id]: __buildGameTree(id) | |
}; | |
} | |
let rootVideoId = played[0]; | |
function __buildGameTree(id) { | |
return Object.entries(videos[id]) | |
.filter(([cardLabel, cardVideoId]) => cardVideoId !== rootVideoId) | |
.map(([cardLabel, cardVideoId]) => [cardLabel, cardVideoId, __buildGameTree(cardVideoId)]) | |
.reduce((obj, [cardLabel, cardVideoId, tree]) => (obj[`${cardLabel}: ${cardVideoId}`] = tree, obj), {}); | |
} | |
await discoverVideos('PmWQmZXYd74'); | |
let gameTree = buildGameTree(played[0]); | |
console.log(window.treeify && treeify.asTree(gameTree, false) || 'treeify not available'), gameTree; |
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
// no longer works, instead refer to RPS-youtube-descriptions-spider.js | |
// 1. open https://www.youtube.com/embed/PmWQmZXYd74 | |
// 2. run this in the browser's console | |
var videos = {}; | |
var errors = []; | |
var played = []; | |
var queued = []; | |
var player; | |
function onYouTubeIframeAPIReady() { | |
player = new YT.Player('player', { | |
height: '390', | |
width: '640', | |
videoId: 'PmWQmZXYd74', | |
playerVars: { | |
'autoplay': 1, | |
'playsinline': 1 | |
}, | |
events: { | |
'onError': onError, | |
'onStateChange': onPlayerStateChange | |
} | |
}); | |
} | |
function playNext() { | |
if (!queued.length) { | |
setTimeout(playNext, 1000); | |
return; | |
} | |
var nextVideo = queued.shift(); | |
console.log(`Loading video ${nextVideo}`); | |
player.loadVideoById(nextVideo); | |
} | |
function onError(event) { | |
errors.push({ | |
videoId: event.target.getVideoData().video_id, | |
...event | |
}); | |
console.error(event); | |
playNext(); | |
} | |
function onPlayerStateChange(event) { | |
if (event.data == YT.PlayerState.PLAYING) { | |
player.pauseVideo(); | |
var id = player.getVideoData().video_id; | |
var cards = Array.from(player.g.contentDocument.querySelectorAll('.ytp-ce-covering-overlay')) | |
.map(card => card.href.replace(/.+=/, '')) | |
.filter(card => !card.startsWith('PL')); | |
played.push(id); | |
if (cards) { | |
videos[id] = { | |
id: id, | |
cards: cards, | |
depth: [0], | |
rank: [0], | |
loadedBy: [], | |
...videos[id] | |
}; | |
var uniqueCards = 0; | |
var currentCard = 0; | |
cards.forEach(card => { | |
if (videos[card]) { | |
console.debug(`${card} has already been discovered, loaded by ${videos[card].loadedBy}`); | |
} else { | |
uniqueCards++; | |
console.debug(`${card} queuing`); | |
queued.push(card); | |
videos[card] = { | |
depth: [], | |
rank: [], | |
loadedBy: [] | |
}; | |
} | |
// I started to implement depth/rank to aid in building a visualization, incomplete | |
videos[card].depth.push(videos[id].depth[0] + 1); | |
videos[card].rank.push(getCardRank(cards.length, currentCard++)); | |
videos[card].loadedBy.push(id); | |
}); | |
console.log(`Video ${id} had ${cards.length} cards (${uniqueCards} new)`); | |
console.log(`${played.length} videos played, ${queued.length} queued`); | |
playNext(); | |
} | |
} | |
} | |
function getCardRank(total, current) { | |
if (total === 1 && current === 0) return 0; | |
if (total === 2 && current === 0) return -1; | |
if (total === 2 && current === 1) return 1; | |
if (total === 3 && current === 0) return -1; | |
if (total === 3 && current === 1) return 0; | |
if (total === 3 && current === 2) return 1; | |
} | |
var tag = document.createElement('script'); | |
tag.src = "https://www.youtube.com/iframe_api"; | |
var firstScriptTag = document.getElementsByTagName('script')[0]; | |
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment