using PyCall
using Conda
using Graphs
using WGLMakie
using CairoMakie
using GraphMakie
using GraphMakie.NetworkLayout
using JSON3
using JSServe, Markdown
using ColorSchemes
Page(exportable=true, offline=true)
activate!()
WGLMakie.inline!(true) Makie.
Graphing CGP Grey’s Rock-Paper-Scissors YouTube Game
python, julia, cgpgrey rock paper scissors youtube game
If you haven’t already checked it out, go watch CGPGrey’s Rock-Paper-Scissors YouTube Game.
In this post, I’m going to explore what all the possible paths available are. Let’s import some packages first.
Fortunately for us, CGPGrey was kind enough to put links to the choices in the description of (almost) every video. We can use Google’s YouTube API to get the video descriptions and get all the YouTube links in the description.
We are going to use the google-api-python-client
in Python from Julia.
Code
= ENV["YOUTUBE_API_KEY"]; # Get API_KEY from google console
API_KEY = pyimport("googleapiclient.discovery").build # from googleapiclient.discovery import build
build = build("youtube", "v3", developerKey=API_KEY) # call build function in Python youtube
Now we can get the description of every video, extract the metadata from it into a Dict
of Dict
s, build a graph:
Code
youtubeid(url) = string(first(split(replace(url, "https://www.youtube.com/watch?v=" => ""), "&t")))
function metadata(url)
= youtubeid(url)
id = youtube.videos().list(part=["snippet", "statistics"], id=id)
request = request.execute()
response = response["items"][1]["snippet"]["description"]
description = response["items"][1]["snippet"]["title"]
title = parse(Int, response["items"][1]["statistics"]["viewCount"])
views if url == "https://www.youtube.com/watch?v=CPb168NUwGc"
# Special case for https://www.youtube.com/watch?v=CPb168NUwGc (description for this is not standard)
return (; description="""
WIN: https://www.youtube.com/watch?v=RVLUX6BUEJI
LOSE / DRAW: https://www.youtube.com/watch?v=jDQqv3zkbIQ
🌐 Website: https://www.cgpgrey.com
💖 Patreon: https://www.patreon.com/cgpgrey
📒 Cortex: http://www.cortexbrand.com
⛔️ Ah01F ✅
""", title="🔴", views)
end
(; description, title, views)end
function links(url; visited=Dict(), duplicate_links=false)
= metadata(url)
m = Dict(
r :id => youtubeid(url),
:code => last(split(strip(m.description), "\n")), # last line is a special code
:url => url,
:links => [],
:children => [],
:title => m.title,
:views => m.views,
)for line in split(m.description, "\n")
if occursin("https://www.youtube.com/watch?v=", line)
= split(line, ":", limit=2)
_status, video = strip(video)
video push!(r[:links], Dict(:status => string(_status), :url => string(video)))
end
end
for link in r[:links]
= link[:url]
url if !(url in keys(visited))
= Dict()
visited[url] = links(url; visited, duplicate_links)
s push!(r[:children], s)
= s
visited[url] else
&& push!(r[:children], visited[url])
duplicate_links end
end
return r
end
function cached_links(url; duplicate_links)
= """$(youtubeid(url))-$(duplicate_links ? "dup-links" : "no-dup-links").json"""
bfile if isfile(bfile)
return JSON3.read(bfile)
end
= links(url; duplicate_links)
r open(bfile, "w") do f
write(f, r)
JSON3.end
rend
function _clean_titles(str)
= join([c for c in str if isascii(c)])
t = strip(t)
t if occursin("Cortex", t)
return ""
end
string(t)
end
function _node_builder(nodes, d)
for c in d[:children]
push!(nodes, (; id=c[:id], title=_clean_titles(c[:code]), url=c[:url], views=c[:views]))
_node_builder(nodes, c)
end
end
function _graph_builder(G, d, ids)
= d[:id]
from for c in d[:children]
= c[:id]
to add_edge!(G, findfirst(isequal(from), ids), findfirst(isequal(to), ids))
_graph_builder(G, c, ids)
end
end
function get_nodes(data)
= [(; id=data[:id], title=_clean_titles(data[:title]), url=data[:url], views=data[:views])]
nodes _node_builder(nodes, data)
= unique(nodes)
nodes = [n.id for n in nodes]
ids = [n.title for n in nodes]
titles = [n.url for n in nodes]
urls
(; ids, titles, urls, nodes)end
function grapher(data, ids)
= SimpleDiGraph(length(ids))
G _graph_builder(G, data, ids)
Gend
= cached_links("https://www.youtube.com/watch?v=PmWQmZXYd74", duplicate_links=true)
data = get_nodes(data)
(; ids, titles, urls, nodes) = grapher(data, ids) G
{111, 206} directed simple Int64 graph
There’s 111 videos in this graph with 206 connections between the videos.
Here’s what that graph visualized looks like:
Code
activate!()
WGLMakie.set_theme!(; resolution=(1600, 900), fonts=(; title="CMU Serif"))
= [node[:views] for node in nodes]
views = extrema(views[2:end])
min_val, max_val = (views .- min_val) ./ (max_val - min_val)
normed_views = cgrad(:viridis, scale=log)
colors = ColorSchemes.get.(Ref(colors), normed_views)
node_colors
= graphplot(G;
f, ax, p =titles,
nlabels=10,
nlabels_fontsize=node_colors,
node_color=20,
node_size=8,
arrow_size=Stress(dim=3)
layout
)Colorbar(f[1, 2], limits=extrema(views), colormap=colors, label="YouTube Views")
# hidedecorations!(ax); hidespines!(ax);
# offsets = [Point2f(0.1, -0.5) for _ in p[:node_pos][]]
# offsets[1] = Point2f(0.1, 0.5)
# p.nlabels_offset[] = offsets
# autolimits!(ax)
# ax.title = "CGP Grey's Rock-Paper-Scissors YouTube Game"
f