Remove graph lib, fix race condition, parameterize video id
This commit is contained in:
parent
e2514185e8
commit
bb74afb9ec
|
@ -18,7 +18,7 @@ export const getPlayerConfig = videoId =>
|
||||||
|
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise((resolve, reject) =>
|
||||||
{
|
{
|
||||||
request(url, (err, res, body) =>
|
request(url, (err, _, body) =>
|
||||||
{
|
{
|
||||||
if (err)
|
if (err)
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
|
|
||||||
const GRAPH_NODE = ' "%s" [label="%s"]';
|
const YOUTUBE_WATCH = 'https://youtu.be/%s';
|
||||||
const GRAPH_NODE_URL = ' "%s" [label="%s", URL="%s", fontcolor=blue]';
|
const GRAPH_NODE = ' "%s" [label="%s", URL="%s", fontcolor=blue]';
|
||||||
const GRAPH_LINK = ' "%s" -> "%s"';
|
const GRAPH_LINK = ' "%s" -> "%s"';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,45 +12,33 @@ const GRAPH_LINK = ' "%s" -> "%s"';
|
||||||
const escapeQuotes = str => str.replace(/"/g, '\\"');
|
const escapeQuotes = str => str.replace(/"/g, '\\"');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a graph to the DOT format.
|
* Convert a graph of videos to the DOT format.
|
||||||
*
|
*
|
||||||
* @param graph Graph to convert.
|
* @param nodes Nodes of the graph.
|
||||||
|
* @param next For each node, list of next neighbors.
|
||||||
* @param [title=identity] Function giving the name of each node.
|
* @param [title=identity] Function giving the name of each node.
|
||||||
* @param [url=none] Function given the URL of each node, or an empty string
|
* @param [url=none] Function given the URL of each node, or an empty string
|
||||||
* if a node has no URL.
|
* if a node has no URL.
|
||||||
* @return DOT representation of the graph.
|
* @return DOT representation of the graph.
|
||||||
*/
|
*/
|
||||||
export const graphToDOT = (graph, title = id => id, url = () => '') =>
|
export const graphToDOT = (nodes, next) =>
|
||||||
{
|
{
|
||||||
const ser = graph.serialize();
|
|
||||||
|
|
||||||
// Convert nodes
|
// Convert nodes
|
||||||
const nodes = ser.nodes.map(({id}) =>
|
const nodesStr = Object.entries(nodes).map(([id, {title}]) =>
|
||||||
{
|
{
|
||||||
const nodeTitle = title(id);
|
const url = util.format(YOUTUBE_WATCH, id);
|
||||||
const nodeUrl = url(id);
|
return util.format(GRAPH_NODE, id, escapeQuotes(title), url);
|
||||||
|
}).join('\n');
|
||||||
if (url === '')
|
|
||||||
{
|
|
||||||
return util.format(GRAPH_NODE, id, escapeQuotes(nodeTitle));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return util.format(GRAPH_NODE_URL, id, escapeQuotes(nodeTitle), nodeUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).join('\n');
|
|
||||||
|
|
||||||
// Convert edges
|
// Convert edges
|
||||||
const links = ser.links.map(({source, target}) =>
|
const nextStr = Object.entries(next).map(([id, neighbors]) =>
|
||||||
util.format(GRAPH_LINK, source, target)
|
Array.from(neighbors)
|
||||||
|
.map(neighbor => util.format(GRAPH_LINK, id, neighbor))
|
||||||
|
.join('\n')
|
||||||
).join('\n');
|
).join('\n');
|
||||||
|
|
||||||
return (
|
return `digraph youtube {
|
||||||
'digraph epenser {\n'
|
${nodesStr}
|
||||||
+ nodes
|
${nextStr}
|
||||||
+ '\n'
|
}`;
|
||||||
+ links
|
|
||||||
+ '\n}'
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +1,26 @@
|
||||||
import Graph from 'graph-data-structure';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
|
|
||||||
import * as api from './api.mjs';
|
import * as api from './api.mjs';
|
||||||
import {graphToDOT} from './graph.mjs';
|
import {graphToDOT} from './graph.mjs';
|
||||||
|
|
||||||
const YOUTUBE_WATCH = 'https://youtu.be/%s';
|
|
||||||
|
|
||||||
// Fetch the output path from command line
|
// Fetch the output path from command line
|
||||||
if (process.argv.length !== 3)
|
if (process.argv.length !== 4)
|
||||||
{
|
{
|
||||||
console.error(`Usage: node explore [output]`);
|
console.error(`Usage: ${process.argv[1]} ROOT DEST
|
||||||
|
Explore videos linked from ROOT and write the resulting graph to DEST.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dest = process.argv[2];
|
const root = process.argv[2];
|
||||||
|
const dest = process.argv[3];
|
||||||
// Graph of visited videos. Each node is a video which is linked to all the
|
|
||||||
// videos to which there is a link, either through a card or an endscreen item
|
|
||||||
const videosGraph = Graph();
|
|
||||||
|
|
||||||
// Store metadata about each visited video
|
// Store metadata about each visited video
|
||||||
const videosMeta = Object.create(null);
|
const videosNodes = Object.create(null);
|
||||||
|
|
||||||
|
// List of videos linked from each video either through a card or an
|
||||||
|
// endscreen item
|
||||||
|
const nextVideos = Object.create(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively explore a video and the video linked from it to fill
|
* Recursively explore a video and the video linked from it to fill
|
||||||
|
@ -33,40 +31,33 @@ const videosMeta = Object.create(null);
|
||||||
const exploreVideo = async videoId =>
|
const exploreVideo = async videoId =>
|
||||||
{
|
{
|
||||||
// Make sure we don’t explore the same video twice
|
// Make sure we don’t explore the same video twice
|
||||||
if (videoId in videosMeta)
|
if (videoId in videosNodes)
|
||||||
{
|
{
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerConfig = await api.getPlayerConfig(videoId);
|
videosNodes[videoId] = {};
|
||||||
videosMeta[videoId] = api.getVideoMeta(playerConfig);
|
|
||||||
|
|
||||||
const linkedVideos = [
|
const playerConfig = await api.getPlayerConfig(videoId);
|
||||||
...api.getEndScreenVideos(playerConfig),
|
videosNodes[videoId] = api.getVideoMeta(playerConfig);
|
||||||
...api.getCardVideos(playerConfig),
|
nextVideos[videoId] = new Set();
|
||||||
];
|
|
||||||
|
|
||||||
// Add links between this video and the linked ones
|
// Add links between this video and the linked ones
|
||||||
linkedVideos.forEach(id => videosGraph.addEdge(videoId, id));
|
api.getEndScreenVideos(playerConfig)
|
||||||
|
.forEach(nextId => nextVideos[videoId].add(nextId));
|
||||||
|
api.getCardVideos(playerConfig)
|
||||||
|
.forEach(nextId => nextVideos[videoId].add(nextId));
|
||||||
|
|
||||||
// Recurse on linked videos
|
// Recurse on linked videos
|
||||||
return Promise.all(linkedVideos.map(id => exploreVideo(id)));
|
return Promise.all(
|
||||||
|
Array.from(nextVideos[videoId])
|
||||||
|
.map(id => exploreVideo(id))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Metadata of the source video
|
|
||||||
const rootVideoId = 'EZGra6O8ClQ';
|
|
||||||
console.log('Starting to explore!');
|
console.log('Starting to explore!');
|
||||||
|
exploreVideo(root).then(() =>
|
||||||
exploreVideo(rootVideoId).then(() =>
|
|
||||||
{
|
{
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(dest, graphToDOT(videosNodes, nextVideos));
|
||||||
dest,
|
console.log(`Finished. Result in ${dest}`);
|
||||||
graphToDOT(
|
|
||||||
videosGraph,
|
|
||||||
id => videosMeta[id].title,
|
|
||||||
id => util.format(YOUTUBE_WATCH, id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Finished. Result in ' + dest);
|
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": {
|
"ajv": {
|
||||||
"version": "6.12.3",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
@ -39,9 +39,9 @@
|
||||||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
||||||
},
|
},
|
||||||
"aws4": {
|
"aws4": {
|
||||||
"version": "1.10.0",
|
"version": "1.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
|
||||||
"integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA=="
|
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
|
||||||
},
|
},
|
||||||
"bcrypt-pbkdf": {
|
"bcrypt-pbkdf": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
@ -134,22 +134,17 @@
|
||||||
"assert-plus": "^1.0.0"
|
"assert-plus": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"graph-data-structure": {
|
|
||||||
"version": "1.12.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/graph-data-structure/-/graph-data-structure-1.12.1.tgz",
|
|
||||||
"integrity": "sha512-0DHxFEUk2EHO19PQrcOckz91WZPk7Itl2mmNpGdpSIZUtBUHRVJPtuuZCJAXB69YRL4fKDCRv2cX3ly8aZZ0QQ=="
|
|
||||||
},
|
|
||||||
"har-schema": {
|
"har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||||
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
|
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
|
||||||
},
|
},
|
||||||
"har-validator": {
|
"har-validator": {
|
||||||
"version": "5.1.3",
|
"version": "5.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
|
||||||
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
|
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"ajv": "^6.5.5",
|
"ajv": "^6.12.3",
|
||||||
"har-schema": "^2.0.0"
|
"har-schema": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -270,9 +265,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
@ -318,9 +313,9 @@
|
||||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
||||||
},
|
},
|
||||||
"uri-js": {
|
"uri-js": {
|
||||||
"version": "4.2.2",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
|
||||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
"integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "mkdir -p build && node explore/index.mjs build/maze.dot && dot -Tsvg build/maze.dot -o build/maze.svg"
|
"epenser": "mkdir -p build && node explore/index.mjs EZGra6O8ClQ build/epenser.dot && dot -Tsvg build/epenser.dot -o build/epenser.svg",
|
||||||
|
"defakator": "mkdir -p build && node explore/index.mjs XM1ssJ8yxdg build/defakator.dot && dot -Tsvg build/defakator.dot -o build/defakator.svg"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graph-data-structure": "^1.12.1",
|
|
||||||
"request": "^2.88.2"
|
"request": "^2.88.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue