LetterJumpd

How I built this

LetterJumpd answers one thing: how are any two films connected? Pick a start and an end, and it finds the shortest chain between them, where every hop is a person both films share. Below is the actual build. The data, the graph, the calls that change what you see, and everything that broke on the way.

Chapter 01

The idea

There's a trend where people race to link two films from memory. Start here, jump through a shared face, land there. Kal Ho Naa Ho to The Wolf of Wall Street in three: Shah Rukh Khan carries you into another Bollywood film, Amitabh Bachchan carries you into The Great Gatsby, DiCaprio takes you home. It's a good game and a terrible thing to verify. The claim is the fun part. The proof is the part nobody has.

That gap is the product. Oracle of Bacon has solved actor-to-actor for decades, and there are daily puzzle games where the film pair gets chosen for you. But nobody lets you pick any two films and watch the route assemble itself. So: two boxes, a jump, and a chain that fills the gap one hop at a time.

I decided early that the reveal was the whole thing. If the answer just appeared as a list, none of the rest would matter. You'd read it once and forget it. Everything underneath exists to make that one moment fast enough and true enough to be worth watching.

Chapter 02

Where the data comes from

All of it is TMDB, the same source Letterboxd itself uses. I never scraped Letterboxd and won't. Their terms say no, and the trend is the inspiration here, not the dataset. Everything you see, down to the posters, comes through TMDB's public API with attribution shown wherever the data is.

A hop, meaning one step in the chain, needs a person both films share. So the useful unit is the credit: this person, on this film, in this role. I keep five kinds (cast, director, composer, cinematographer, writer) and drop the rest. Producers were a deliberate cut. They're listed in their dozens on big films and connect everything to everything, which makes for technically valid chains that tell you nothing about the movies.

TMDB's terms ended up shaping this build more than any design call did. Nothing can be cached beyond six months, so syncing “what changed today” isn't enough on its own. A film nobody edits would just sit here forever, quietly going stale and out of compliance. So a scheduled job runs every night. It pulls new titles, re-fetches whatever TMDB reports as changed, and re-pulls a rolling slice of the whole catalogue, sized so every row gets renewed well inside that window. The same terms rule out AI features built on their data, so there's no chatbot bolted to the side of this, and there won't be.

Chapter 03

Crawling a million films

I started with the 20,000 most popular films just to get something running. That exposed a bias immediately: ranking by popularity buries older Hindi cinema. Kal Ho Naa Ho sits somewhere around position thirty thousand, so my own test case wasn't in my own dataset. The seed was scaffolding, not the product.

So, the full crawl. 14.4 hours, 1.17M requests, one call per film with its credits attached, run at about 25 a second. That sits well under TMDB's rate limit, the cap they put on how many requests one client may fire per second, because there was no reason to crowd them. TMDB hit two rough patches overnight and errored on roughly 1,900 films. A second pass cleared every one in 90 seconds, and that's the part I'm actually pleased with. The crawler keeps its work queue in the database, so a failure is just a row waiting to be retried. It trusts the queue, not its own memory.

I tested that by killing the process outright mid-run. The database came back consistent, the restart re-fetched only what was still pending, and nothing duplicated. Which turned out to be lucky rehearsal, because my laptop ran itself down to 9% battery in the middle of the real crawl. Same design, same shrug, instead of starting over.

1.18M

films

2.97M

people

10.3M

credits

10.5k

Hindi films

Chapter 04

The graph, and why it’s fast

Films and people form one bipartite graph. Bipartite meaning every node is either a film or a person, and every link runs between the two kinds, never film to film. There are 9.6M links, each carrying a bitmask: a handful of bits recording which roles apply to that pair, so one link can say “cast here, director there” at once. That mask is what makes the toggles instant. Turning composer on doesn't rebuild anything, it just widens the set of links the search may walk.

The whole thing compiles into flat arrays, one long run of numbers rather than a million little objects, which the server memory-maps at startup. That means it points at the file on disk and lets the operating system pull in pages as they get touched, instead of loading 171 MB into memory up front. Answering queries costs about 250 MB even though building the artifact peaks past 2 GB.

A query runs a bidirectional search. It explores outward from the start film and the end film at the same time, always expanding whichever side currently has fewer nodes to look at. That matters more than it sounds. A hub actor can carry hundreds of films, so searching from one end alone drowns in them, while two searches meet in the middle after a fraction of the work. Where they touch gives the exact shortest length.

From there it doesn't stop at one answer. It reconstructs the layer of routes that share that length, counts them exactly, and ranks them. So the header says “1 of 63 shortest routes” and means precisely 63, not an estimate. Ties break the same way every time, which is the boring detail that makes a shared link replay the same route for whoever opens it. Half of all queries answer inside 6 ms, and 95% inside 29 ms.

52 films 70 linking people319 linksdrag to spin · click a dot to isolate it

That's a real slice of the graph above, not an illustration: 52 well-known films and the 70 people who link at least two of them, pulled straight from the database and laid out in three dimensions. Spin it, or grab a dot and pull. The full thing is the same shape, roughly eighty thousand times over.

One number I didn't expect: 42% of random film pairs have no connection at all, at any depth. I checked whether the hop limit was hiding routes. It wasn't. They're obscure titles whose handful of credited people never worked anywhere else, little islands floating off the main mass of cinema. For films people have actually heard of, it's under 1%.

Chapter 06

Decisions that change the answer

Bit parts are off by default. The first honest answer between Kal Ho Naa Ho and The Wolf of Wall Street was two hops, through an uncredited extra listed as “Bagel Vendor”. Both films shot in New York and used the same local crowd. Technically the shortest, completely useless. Cast billed below tenth and anything marked uncredited now sit behind a deep cuts toggle. Leave them off and the route becomes Shah Rukh Khan to Amitabh Bachchan to Leonardo DiCaprio: three hops, and the one you actually wanted. Nothing is deleted. The extras are still in the database, one switch away.

Then there was the question of how many routes to show. Every possible path between two films runs into the millions the moment a hub actor shows up, and means nothing. Every shortest path is a small, countable set that falls out of the same search for free. So that's what “find all connections” means here.

Ranking leans toward films you'd recognise, because a chain is only satisfying if you know the faces in it, so routes get scored by how well-known their films and people are. It isn't perfect yet. The Hollywood-heavy route still edges out the Shah Rukh one on the default sort, and I want to tune that.

Synopses get fetched on demand. Storing a plot summary for a million films meant another 14-hour crawl for text almost nobody reads. So they get pulled for whatever's on your screen the moment a route loads, then cached. The first look costs a network trip. Every look after that is instant.

Chapter 07

Making it feel like something

A correct answer rendered as a list is forgettable, so the route arrives as choreography. The two films you picked glide apart to make room, then each hop draws its wire and pops the next film into place, about two seconds end to end for a four-hop chain. Everything eases out rather than in, and nothing bounces. The pop is a 3% overshoot, enough to feel alive and not enough to feel like a toy.

Long chains used to run off the side of the screen behind a scrollbar, which is a miserable way to read a route. Now it zig-zags, ends on a baseline and middles alternating above and below, and the layout picks how wide to run from your viewport, with zoom and pan for the long ones. The labels sit above the wires instead of across them, because at four hops the pills were covering the very thing they were labelling.

Hover any film for its poster, runtime, director, top cast and synopsis, plus links out to Letterboxd and TMDB. And if you have reduced motion turned on, which is the accessibility setting that tells sites to stop animating, you get exactly the same information with none of the movement. The reveal is a nice-to-have. The answer isn't.

Chapter 08

What broke

A rate limiter that hung forever, because it waited for a counter to reach exactly 1.0, and floating point (the way computers approximate decimals) never quite lands on a round number. It surfaced as a test that simply never finished. No error, no output, just a process sitting there.

A hot swap that compared folder names. Hot swap here means replacing the graph a running server is using without restarting it, so nobody sees an outage. Because it compared names, rebuilding twice in one day changed nothing, which is exactly what a manual re-run does. I fixed it with a build timestamp, then had to move that to nanosecond precision, because two rebuilds inside the same second looked identical too.

A “previous route” button that wrapped from route 1 back to route 1,350 and got a 422 back, which is the API's way of saying the request was out of range, since it caps how deep you can page. A logo whose letterforms merged into a plain “U”. Hover cards sliding off the canvas edge. And macOS quietly marking a Python import file as hidden, which Python 3.13 then refused to read. That one cost an hour and had nothing to do with this project at all.

33 journal entries, one per milestone, bug and decision. I kept that log honest while building rather than tidying it afterwards, which is the only reason I can write this chapter accurately.

Chapter 09

What’s next

The branching view is the big one: every shortest route at once, as a web instead of a single line. The API already returns it, counted and ranked, so it's a drawing problem, not a data problem.

After that, a share image per route so a link posted anywhere carries its own chain, and IMDb alongside the Letterboxd and TMDB links. Then race mode, which was in the first sketch and got deferred. You guess the chain, then the answer reveals and tells you how close you were.

And a rate limit before I share the link widely, because right now the API is open and unauthenticated. The catalogue keeps refreshing nightly either way.

Chapter 10

Colophon

Python, SQLite and FastAPI behind the graph. Next.js and Tailwind in front. Instrument Serif and Geist for type, amber on warm near-black. Posters and metadata from TMDB.

Aditya Kishtawal

I'm Aditya Kishtawal. I built this, from the idea to the crawl to the amber.

This product uses TMDB and the TMDB APIs but is not endorsed, certified, or otherwise approved by TMDB. Inspired by the Letterboxd speedrun trend; not affiliated with Letterboxd.

← Back to the jump