├── .gitignore ├── hurley ├── __init__.py ├── podcasts.py ├── templates │ ├── about.html │ ├── index.html │ ├── privacy.html │ └── base.html ├── dijkstra.py └── server.py ├── run.py ├── hurley.wsgi ├── README.md └── data.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .venv 3 | -------------------------------------------------------------------------------- /hurley/__init__.py: -------------------------------------------------------------------------------- 1 | from hurley.server import app 2 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from hurley.server import app 4 | 5 | if __name__ == '__main__': 6 | app.run(host='0.0.0.0') 7 | -------------------------------------------------------------------------------- /hurley.wsgi: -------------------------------------------------------------------------------- 1 | import sys 2 | import site 3 | 4 | sdir = "/home/alexwlchan/.virtualenvs/hurley/local/lib/python2.7/site-packages" 5 | site.addsitedir(sdir) 6 | 7 | # Expand Python classes path with your app's path 8 | sys.path.insert(0, "/var/www/six-degrees-of-myke/hurley") 9 | 10 | from hurley import app 11 | 12 | # Put logging code (and imports) here ... 13 | 14 | # Initialize WSGI app object 15 | application = app 16 | -------------------------------------------------------------------------------- /hurley/podcasts.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import json 3 | 4 | from hurley.dijkstra import Graph 5 | 6 | import os 7 | with open("/var/www/six-degrees-of-myke/hurley/data.json") as f: 8 | data = json.loads(f.read()) 9 | 10 | pairings = set() 11 | podcasters = set() 12 | for value in data.values(): 13 | for person in value: 14 | podcasters.add(person) 15 | for i in itertools.permutations(value, 2): 16 | pairings.add(i) 17 | pairings = list(pairings) 18 | 19 | graph = Graph(pairings) 20 | 21 | 22 | def find_links(result): 23 | pairings = [] 24 | for i in xrange(len(result) - 1): 25 | pair = list(result)[i:i+2] 26 | for podcast, podcasters in data.iteritems(): 27 | if all(item in podcasters for item in list(pair)): 28 | pairings.append((pair[0], pair[1], podcast)) 29 | return pairings 30 | -------------------------------------------------------------------------------- /hurley/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 7 | 8 |

9 | This started as a joke in episode #15 of Cortex, a podcast on the Relay.FM network. 10 |

11 | 12 |

13 | The hosts were joking about the Six Degrees of Kevin Bacon, and how Myke is the Kevin Bacon of the podcasting world. 14 | He founded his own podcast network, and co-hosts many of the shows. 15 | They were wondering how many connections you could find from Myke to other popular podcasters. 16 |

17 | 18 |

19 | Lots of people were making static charts, but those are hard to follow and don't scale to many podcasters. 20 | I made this site as a bit of fun while cooking dinner. 21 |

22 | 23 |

24 | It serves no purpose, it's just a bit of fun. I hope it makes you smile. :-) 25 |

26 | 27 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Six Degrees of Myke Hurley](http://six-degrees-of-myke.net/) 2 | 3 | This is a quick Flask app for finding links between podcasters, a la the [Six Degrees of Kevin Bacon](https://en.wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon) game. It's a bit of nonsense that I hope will make people smile. 4 | 5 | For the slightly longer description, see the [about page](http://six-degrees-of-myke.net/about). 6 | 7 | ### The code 8 | 9 | This is just a simple Flask app that turns a list of podcasts and hosts into a graph, then uses [Dijkstra's algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) to find the shortest path between two hosts (if one exists). 10 | The code for Dijkstra is taken from [Rosetta code](http://rosettacode.org/wiki/Dijkstra%27s_algorithm), because I threw this together in a hurry. 11 | 12 | Most of this is quite messy because I threw it together quickly. It's just a bit of fun, not a serious project. 13 | 14 | ### Contributing 15 | 16 | If you want to expand the list of podcasts, you should edit `data.json`. Each podcast gets an entry of the form `name: [list of hosts]`; for example, 17 | 18 | ```json 19 | "Hello Internet": [ 20 | "Brady Haran", 21 | "CGP Grey" 22 | ] 23 | ``` 24 | 25 | Please keep the host names alphabetically ordered, and likewise the podcasts. 26 | 27 | Currently I have all the Relay.FM and 5by5 shows, plus a few others. More are always welcome. :-) 28 | -------------------------------------------------------------------------------- /hurley/dijkstra.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This does the heavy lifting; it uses Djiksta's algorithm to find the 4 | shortest path between podcast hosts. 5 | 6 | Heavily cribbed from http://rosettacode.org/wiki/Dijkstra%27s_algorithm#Python 7 | """ 8 | 9 | from collections import namedtuple, deque 10 | import itertools 11 | import json 12 | from pprint import pprint as pp 13 | 14 | 15 | inf = float('inf') 16 | Edge = namedtuple('Edge', ['start', 'end']) 17 | 18 | 19 | class Graph(): 20 | def __init__(self, edges): 21 | self.edges = edges2 = [Edge(*edge) for edge in edges] 22 | self.vertices = set(sum(([e.start, e.end] for e in edges2), [])) 23 | 24 | def dijkstra(self, source, dest): 25 | assert source in self.vertices 26 | dist = {vertex: inf for vertex in self.vertices} 27 | previous = {vertex: None for vertex in self.vertices} 28 | dist[source] = 0 29 | q = self.vertices.copy() 30 | neighbours = {vertex: set() for vertex in self.vertices} 31 | for start, end in self.edges: 32 | neighbours[start].add((end, 1)) 33 | # pp(neighbours) 34 | 35 | while q: 36 | u = min(q, key=lambda vertex: dist[vertex]) 37 | q.remove(u) 38 | if dist[u] == inf or u == dest: 39 | break 40 | for v, cost in neighbours[u]: 41 | alt = dist[u] + cost 42 | if alt < dist[v]: # Relax (u,v,a) 43 | dist[v] = alt 44 | previous[v] = u 45 | # pp(previous) 46 | s, u = deque(), dest 47 | while previous[u]: 48 | s.appendleft(u) 49 | u = previous[u] 50 | s.appendleft(u) 51 | return s 52 | -------------------------------------------------------------------------------- /hurley/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from flask import Flask, render_template 4 | from flask.ext.wtf import Form 5 | from wtforms import SelectField 6 | 7 | from hurley.podcasts import pairings, podcasters, graph, find_links 8 | 9 | 10 | app = Flask(__name__) 11 | key = 'no really tis must dosidhksjdfgnvkyjrhmstbgnemr,test secret key' 12 | app.config['WTF_CSRF_ENABLED'] = True 13 | app.config['SECRET_KEY'] = key 14 | 15 | 16 | MYCACHE = {} 17 | 18 | 19 | class PodcastForm(Form): 20 | choices = [("", "")] + [(p, p) for p in sorted(podcasters)] 21 | src = SelectField('src', choices=choices) 22 | dst = SelectField('dst', choices=choices) 23 | 24 | 25 | @app.route('/', methods=['GET', 'POST']) 26 | def hello_world(): 27 | form = PodcastForm() 28 | result = None 29 | print MYCACHE 30 | if form.validate_on_submit() and form.src.data and form.dst.data: 31 | pairings = MYCACHE.setdefault((form.src.data, form.dst.data), 32 | graph.dijkstra(form.src.data, 33 | form.dst.data)) 34 | if form.src.data == form.dst.data: 35 | result = ['identical'] 36 | elif len(pairings) == 1: 37 | result = ["disconnected"] 38 | else: 39 | result = find_links(pairings) 40 | 41 | return render_template('index.html', 42 | form=form, 43 | src=form.src.data, 44 | dst=form.dst.data, 45 | result=result) 46 | 47 | 48 | @app.route('/about') 49 | def about(): 50 | return render_template('about.html', title="About") 51 | 52 | 53 | @app.route('/privacy') 54 | def privacy(): 55 | return render_template('privacy.html', title="Privacy") 56 | -------------------------------------------------------------------------------- /hurley/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {{ form.hidden_tag() }} 6 |

Find the podcast connection between

7 |

12 |

and

13 |


18 | 19 |

20 |
21 | 22 | {% if result %} 23 |
24 |
25 | {% if result == ['identical'] %} 26 | {{ src }} is connected to themselves. 27 | {% elif result == ['disconnected'] %} 28 | I don't know of a podcast connection between {{ src }} and {{ dst }}. 29 | {% elif result|count == 1 %} 30 | {{ src }} and {{ dst }} are on the same podcast: {{ result[0][2] }}. 31 | {% else %} 32 |

{{ src }} and {{ dst }} are separated by {{ result|count }} podcasts:

33 | 38 | {% endif %} 39 |
40 | {% endif %} 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /hurley/templates/privacy.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 7 | 8 |

I use Piwik to provide basic analytics for six-degrees-of-myke.net.

9 | 10 |

Piwik is a piece of software that records data about people who visit the site. It installs a cookie to record basic tracking data.

11 | 12 |

What information does Piwik record?

13 | 14 |

For a full list, see What data does Piwik track? on the Piwik support site.

15 | 16 |

I only record the default information collected by the Piwik JavaScript tracker. I don’t use any of the optional add-ons.

17 | 18 |

I can't see which podcast pairs you're looking up.

19 | 20 |

What gets done with the data?

21 | 22 |

I’m only interested in two bits of information:

23 | 24 | 28 | 29 |

Mostly this is a vanity thing – I just like seeing how many people are visiting my sites.

30 | 31 |

I use the self-hosted version of Piwik, which means the data is stored only on my server. It’s not shared with Piwik or other third-parties (such as advertisers). I don’t run advertising on the site, tracking-enabled or otherwise.

32 | 33 |

I want to opt out.

34 | 35 |

Quoting from the Piwik Privacy page:

36 | 37 |
38 |

Opting out of all data collection

39 | 40 |

To opt-out of all web tracking technologies (including Google Analytics and Piwik), using an extension such as Noscript or Ghostery is the safest way: these browser extensions will disable all the known JavaScript trackers and ensure that your browser does not send a request to external tracking servers. If you wish to browse the Internet without your IP address being tracked at all, please consider using Tor Browser which will automatically connect you to the secured and anonymous Tor network.

41 |
42 | 43 |

If you don’t want to install a browser plugin, you can use the opt-out option below. This will set an opt-out cookie in your browser, which tells Piwik to ignore your visits:

44 | 45 | 46 | 47 | 48 | {% endblock %} -------------------------------------------------------------------------------- /hurley/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 21 | 22 | 23 | 24 | Six Degrees of Myke Hurley{% if title %} - {{ title }}{% endif %} 25 | 26 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 97 | 98 |
99 | 100 | {% block content %} 101 | {% endblock %} 102 | 103 |
104 | 105 |
106 |

What is this nonsense?Source on GitHubPrivacy policy

107 | 108 |

Made by @alexwlchan.

109 | 110 |
111 |
112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /data.json: -------------------------------------------------------------------------------- 1 | { 2 | "Amplified": [ 3 | "Dan Benjamin", 4 | "Jim Dalrymple" 5 | ], 6 | "Analog(ue)": [ 7 | "Casey Liss", 8 | "Myke Hurley" 9 | ], 10 | "ATP": [ 11 | "Casey Liss", 12 | "John Siracusa", 13 | "Marco Arment" 14 | ], 15 | "Back to Work": [ 16 | "Dan Benjamin", 17 | "Merlin Mann" 18 | ], 19 | "Bingeworthy: Mr. Robot": [ 20 | "Dan Benjamin", 21 | "Haddie Cooke", 22 | "Maggie Ball" 23 | ], 24 | "Bionic": [ 25 | "Matt Alexander", 26 | "Myke Hurley" 27 | ], 28 | "Bizarre States": [ 29 | "Andrew Bowser", 30 | "Jessica Chobot" 31 | ], 32 | "BONANZA!": [ 33 | "Matt Alexander", 34 | "Myke Hurley" 35 | ], 36 | "Build and Analyze": [ 37 | "Dan Benjamin", 38 | "Marco Arment" 39 | ], 40 | "Cash\u2019s Corner": [ 41 | "Cash Benjamin", 42 | "Dan Benjamin" 43 | ], 44 | "Chewin' It": [ 45 | "Kevin Heffernan", 46 | "Steve Lemme" 47 | ], 48 | "Clockwise": [ 49 | "Dan Moren", 50 | "Jason Snell" 51 | ], 52 | "Clonecast: The Official Orphan Black Podcast": [ 53 | "Mackenzie Donaldson", 54 | "Rob Moden" 55 | ], 56 | "Comic Book Club": [ 57 | "Alex Zalben", 58 | "Justin Tyler", 59 | "Pete LePage" 60 | ], 61 | "Common Sense": [ 62 | "Dan Carlin" 63 | ], 64 | "Connected": [ 65 | "Federico Viticci", 66 | "Myke Hurley", 67 | "Stephen Hackett" 68 | ], 69 | "Cortex": [ 70 | "CGP Grey", 71 | "Myke Hurley" 72 | ], 73 | "Criminal": [ 74 | "Eric Mennel", 75 | "Lauren Spohrer", 76 | "Phoebe Judge" 77 | ], 78 | "Crybabies": [ 79 | "Sarah Thyre", 80 | "Susan Orlean" 81 | ], 82 | "Dear Hank & John": [ 83 | "Hank Green", 84 | "John Green" 85 | ], 86 | "Denzel Washington is the Greatest Actor of All Time Period": [ 87 | "Kevin Avery", 88 | "W. Kamau Bell" 89 | ], 90 | "Dining with Doug and Karen": [ 91 | "Doug Benson", 92 | "Karen Anderson" 93 | ], 94 | "Directional": [ 95 | "Federico Viticci", 96 | "Myke Hurley" 97 | ], 98 | "FEaB": [ 99 | "Matt Mira", 100 | "Scott Mosier" 101 | ], 102 | "Geek Friday": [ 103 | "Faith Korpi", 104 | "Jason Seifer" 105 | ], 106 | "Get Up On This": [ 107 | "Jensen Karp", 108 | "Mathew Robinson" 109 | ], 110 | "Hardcore History": [ 111 | "Dan Carlin" 112 | ], 113 | "Hello Internet": [ 114 | "Brady Haran", 115 | "CGP Grey" 116 | ], 117 | "Hollywood Handbook": [ 118 | "Hayes Davenport", 119 | "Sean Clements" 120 | ], 121 | "How Did This Get Made?": [ 122 | "Jason Mantzoukas", 123 | "June Diane Raphael", 124 | "Paul Scheer" 125 | ], 126 | "Hypercritical": [ 127 | "Dan Benjamin", 128 | "John Siracusa" 129 | ], 130 | "IRL Talk": [ 131 | "Faith Korpi", 132 | "Jason Seifer" 133 | ], 134 | "Isometric": [ 135 | "Brianna Wu", 136 | "Georgia Dow", 137 | "Maddy Myers", 138 | "Steve Lubitz" 139 | ], 140 | "James Bonding": [ 141 | "Matt Gourley", 142 | "Matt Mira" 143 | ], 144 | "Liftoff": [ 145 | "Jason Snell", 146 | "Stephen Hackett" 147 | ], 148 | "Love, Dad": [ 149 | "David Koechner", 150 | "Jeff Ullrich" 151 | ], 152 | "Maltin on Movies": [ 153 | "Baron Vaughn", 154 | "Leonard Maltin" 155 | ], 156 | "Material": [ 157 | "Andy Ihnatko", 158 | "Russell Ivanovic", 159 | "Yasmine Evjen" 160 | ], 161 | "Mike and Tom Eat Snacks": [ 162 | "Michael Ian Black", 163 | "Tom Cavanagh" 164 | ], 165 | "Nerdist Comics Panel": [ 166 | "Adam Beechen", 167 | "Ben Blacker", 168 | "Heath Corson", 169 | "Len Wein" 170 | ], 171 | "Nerdist Podcast": [ 172 | "Chris Hardwick", 173 | "Jonah Ray", 174 | "Matt Mira" 175 | ], 176 | "Nerd Machine's Picking Favorites": [ 177 | "David Coleman", 178 | "Razzle", 179 | "Tyler Labine", 180 | "Zachary Levi" 181 | ], 182 | "Never Not Funny": [ 183 | "Jimmy Pardo", 184 | "Matt Belknap" 185 | ], 186 | "OMFG!": [ 187 | "Deanna Raphael", 188 | "Emily Foster" 189 | ], 190 | "Overtired": [ 191 | "Brett Terpstra", 192 | "Christina Warren" 193 | ], 194 | "Owen and TJ Read the News": [ 195 | "Adam McKay", 196 | "Owen Burke" 197 | ], 198 | "Planet Money": [ 199 | "David Kestenbaum", 200 | "Jacob Goldstein", 201 | "Robert Smith", 202 | "Stacey Vanek Smith", 203 | "Steve Henn" 204 | ], 205 | "Pod Cats": [ 206 | "Amber Kenny", 207 | "Eddie Pepitone", 208 | "Sean Conroy" 209 | ], 210 | "Pop My Culture": [ 211 | "Cole Stratton", 212 | "Vanessa Ragland" 213 | ], 214 | "Professor Blastoff": [ 215 | "David Huntsberger", 216 | "Kyle Dunnigan", 217 | "Tig Notaro" 218 | ], 219 | "Question of the Day": [ 220 | "James Altucher", 221 | "Stephen J. Dubner" 222 | ], 223 | "Radio Diaries": [ 224 | "Joe Richman", 225 | "Sarah Kate Kramer" 226 | ], 227 | "Radiolab": [ 228 | "Jad Abumrad", 229 | "Robert Krulwich" 230 | ], 231 | "Reconcilable Differences": [ 232 | "John Siracusa", 233 | "Merlin Mann" 234 | ], 235 | "Reply All": [ 236 | "Alex Goldman", 237 | "PJ Vogt" 238 | ], 239 | "Road Work": [ 240 | "Dan Benjamin", 241 | "John Roderick" 242 | ], 243 | "Rocket": [ 244 | "Brianna Wu", 245 | "Christina Warren", 246 | "Simone De Rochefort" 247 | ], 248 | "Ronna and Beverly": [ 249 | "Jamie Denbo", 250 | "Jessica Chaffin" 251 | ], 252 | "Rotten Tomatoes": [ 253 | "Grae Drake", 254 | "Matt Atchity" 255 | ], 256 | "Sklarbro Country": [ 257 | "Jason Sklar", 258 | "Randy Sklar" 259 | ], 260 | "StartUp": [ 261 | "Alex Blumberg", 262 | "Lisa Chow" 263 | ], 264 | "Surprisingly Awesome": [ 265 | "Adam Davidson", 266 | "Adam McKay" 267 | ], 268 | "Terrified": [ 269 | "Anna Seregina", 270 | "Dave Ross" 271 | ], 272 | "The Andy Daly Podcast Pilot Project": [ 273 | "Andy Daly", 274 | "Matt Gourley" 275 | ], 276 | "The Apple Sisters": [ 277 | "Kimmy Gatewood", 278 | "Rebekka Johnson", 279 | "Sarah Lowe" 280 | ], 281 | "The Canon": [ 282 | "Amy Nicholson", 283 | "Devin Faraci" 284 | ], 285 | "The Comic Shack": [ 286 | "Dan Benjamin", 287 | "Mois\u00e9s Chiullan" 288 | ], 289 | "The Cracked Podcast": [ 290 | "Jack O'Brien", 291 | "Michael Swaim" 292 | ], 293 | "The Frequency": [ 294 | "Dan Benjamin", 295 | "Haddie Cooke" 296 | ], 297 | "The Ihnatko Almanac": [ 298 | "Andy Ihnatko", 299 | "Dan Benjamin" 300 | ], 301 | "The Indoor Kids": [ 302 | "Emily V Gordon", 303 | "Kumail Nanjiani" 304 | ], 305 | "The Legacy Music Hour": [ 306 | "Brent Weinbach", 307 | "Rob F." 308 | ], 309 | "The Kitchen Sisters": [ 310 | "Davia Nelson", 311 | "Nikki Silva" 312 | ], 313 | "The Pen Addict": [ 314 | "Brad Dowdy", 315 | "Myke Hurley" 316 | ], 317 | "The Prompt": [ 318 | "Federico Viticci", 319 | "Myke Hurley", 320 | "Stephen Hackett" 321 | ], 322 | "the reality SHOW show": [ 323 | "Hayes Davenport", 324 | "Sean Clements" 325 | ], 326 | "The Sylvester Stallone Show": [ 327 | "Paul Scheer", 328 | "Sylvester Stallone" 329 | ], 330 | "The Talk Show": [ 331 | "Dan Benjamin", 332 | "John Gruber" 333 | ], 334 | "The Truth": [ 335 | "Alejandro Kolleeny", 336 | "Diana McCorry", 337 | "Louis Kornfeld" 338 | ], 339 | "Thoroughly Considered": [ 340 | "Dan Provost", 341 | "Myke Hurley", 342 | "Tom Gerhardt" 343 | ], 344 | "Thrilling Adventure Hour": [ 345 | "Ben Acker", 346 | "Ben Blacker" 347 | ], 348 | "Today We Learned": [ 349 | "Dan Casey", 350 | "Razzle" 351 | ], 352 | "Top Four": [ 353 | "Marco Arment", 354 | "Tiffany Arment" 355 | ], 356 | "Topics": [ 357 | "Michael Ian Black", 358 | "Michael Showalter" 359 | ], 360 | "Totally Laime": [ 361 | "Andy Rosen", 362 | "Elizabeth Laime" 363 | ], 364 | "Upgrade": [ 365 | "Jason Snell", 366 | "Myke Hurley" 367 | ], 368 | "U Talkin' U2 To Me?": [ 369 | "Adam Scott", 370 | "Scott Aukerman" 371 | ], 372 | "Virtual": [ 373 | "Myke Hurley", 374 | "Federico Viticci" 375 | ], 376 | "WOMP It Up": [ 377 | "Jessica St. Clair", 378 | "Lennon Parham" 379 | ], 380 | "Who Carted?": [ 381 | "Howard Kremer", 382 | "Kulap Vilaysack" 383 | ] 384 | } 385 | --------------------------------------------------------------------------------