86 lines
2.5 KiB
JavaScript
86 lines
2.5 KiB
JavaScript
import util from 'util';
|
|
import * as api from './api.mjs';
|
|
import { WATCH_BASE, escapeQuotes } from './util.mjs';
|
|
|
|
const GRAPH_NODE = ' "%s" [label="%s", URL="%s", fontcolor=blue]';
|
|
const GRAPH_LINK = ' "%s" -> "%s"';
|
|
|
|
/**
|
|
* Explore the video graph starting from the given root.
|
|
*
|
|
* @async
|
|
* @param string videoId Source video identifier.
|
|
* @param function onUpdate Callback run every time a new video is explored.
|
|
* @return Array.<Object, Object> Nodes and set of neighbors of each node.
|
|
*/
|
|
export const exploreVideos = async (videoId, onUpdate) =>
|
|
{
|
|
// Store metadata about each visited video
|
|
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);
|
|
|
|
// Videos that still need to be explored
|
|
const queue = [videoId];
|
|
|
|
while (queue.length > 0)
|
|
{
|
|
const currentId = queue.shift();
|
|
|
|
if (!(currentId in videosNodes))
|
|
{
|
|
const metadata = await api.getMetadata(currentId);
|
|
const info = api.getVideoInfo(metadata);
|
|
|
|
videosNodes[currentId] = info;
|
|
nextVideos[currentId] = new Set();
|
|
|
|
// Add links between this video and the linked ones
|
|
api.getEndScreenVideos(metadata)
|
|
.forEach(nextId => nextVideos[info.videoId].add(nextId));
|
|
api.getCardVideos(metadata)
|
|
.forEach(nextId => nextVideos[info.videoId].add(nextId));
|
|
|
|
for (let nextId of nextVideos[info.videoId])
|
|
{
|
|
queue.push(nextId);
|
|
}
|
|
}
|
|
|
|
onUpdate(Object.keys(videosNodes).length, queue.length);
|
|
}
|
|
|
|
return [videosNodes, nextVideos];
|
|
};
|
|
|
|
/**
|
|
* Convert a video graph to the GraphViz format.
|
|
*
|
|
* @param Object videosNodes Videos of the graph.
|
|
* @param Object nextVideos For each video, list of next videos.
|
|
* @return string GraphViz representation of the graph.
|
|
*/
|
|
export const toGraphviz = (videosNodes, nextVideos) =>
|
|
{
|
|
// Convert videosNodes
|
|
const nodesStr = Object.entries(videosNodes).map(([id, {title}]) =>
|
|
{
|
|
const url = util.format(WATCH_BASE, id);
|
|
return util.format(GRAPH_NODE, id, escapeQuotes(title), url);
|
|
}).join('\n');
|
|
|
|
// Convert edges
|
|
const nextStr = Object.entries(nextVideos).map(([id, neighbors]) =>
|
|
Array.from(neighbors)
|
|
.map(neighbor => util.format(GRAPH_LINK, id, neighbor))
|
|
.join('\n')
|
|
).join('\n');
|
|
|
|
return `digraph "" {
|
|
${nodesStr}
|
|
|
|
${nextStr}}`;
|
|
};
|