Disclosure: I’m the maker of the app described here (Wochly), and this post was drafted with AI help. Some actions on this account are automated.
I’ve been building a small web app that ranks movies and TV shows by asking “which did you like more?” instead of asking for a star rating. A few parts were more interesting (or more annoying) than I expected, so here are some notes.
1. Pairwise ranking instead of stars
Star ratings bunch up. Most people’s lists end up as a wall of 4s. So the app asks one rough question first (liked it / it was fine / didn’t like it), then places the title inside that group with a hidden binary insertion: it compares the new title against the middle item of the group, halves the range, and repeats.
- It stops after at most 4 comparisons. With a group of 16 or fewer titles that gives an exact spot. Past that it settles on the middle of the remaining range, which is good enough, and you can drag to fix it later.
- Position turns into a score by spreading the group evenly over a fixed band: liked is 6.7 to 10, fine is 3.4 to 6.6, didn’t like is 0 to 3.3. So the score is derived from order, not typed in.
- Movies and TV are separate lists. Early on, a movie could get compared against a show, and the first tester rightly said that made no sense. Now a movie is only ever compared with movies, and a show with shows.
- When you drag-reorder, the whole group gets renumbered and rescored. That’s a small pure function shared by the app and a backfill script, which made fixing old data easy.
2. Where “available on Netflix” comes from
There’s no public Netflix or Prime API, and I didn’t want to fake one. TMDB’s /{movie|tv}/{id}/watch/providers endpoint returns per-country availability (the data comes from JustWatch, which needs attribution). The catch is that the flatrate list mixes real subscriptions with paid add-ons like “HBO Max Amazon Channel”, “… Apple TV channel” and cable on-demand.
What worked:
- An allowlist of about 18 core subscriptions plus a few truly free services (Tubi, Pluto TV, The Roku Channel). Each one maps several TMDB provider ids to one service (Max is 1899 and 384, Paramount+ is 2303, 2616 and 531), so it doesn’t show up twice.
- Anything not on the list, or whose name matches
/channel|on demand/i, is ignored everywhere. Rent and buy are listed separately. - For “best on service X” pages, I use
/discoverwithwith_watch_providersto get candidates, then re-check every title’s providers one by one. Discover is looser than the per-title data, and this removes the add-on false positives. The re-check runs 12 at a time with a small backoff on failure, to be gentle on TMDB. - “Watch” links go to the service’s own search page for the title. Deep links to exact titles aren’t reliably available without partner data.
3. Ranking titles when you have almost no users
A community average from 2 people is noise. So the sort score is the community average once a title has 5+ rankings, and until then it’s TMDB’s score with a Bayesian pull toward 6.5 (300 votes of weight for TV, 500 for movies), so a 10/10 from 3 voters can’t beat an 8.6 from 20,000. Honestly, today almost everything is still on the fallback.
4. Stack, all free tier
- Next.js 15 (App Router) on Vercel Hobby. TMDB calls happen server-side so the key stays private, with fetch revalidation for caching.
- Supabase for auth (email code or Google) and Postgres. Rankings are a plain table with row-level security, and the community average is a SQL view (
avg(score),count(*)grouped by title). - You can rank without an account. Picks go to localStorage and get moved into your account when you sign in. The local copy is only cleared after the server confirms the write, so a flaky connection can’t lose rankings.
- The Letterboxd import parses the export CSV entirely in the browser (a small RFC 4180-style parser that handles quoted commas, doubled quotes, BOM and CRLF), matches films to TMDB by title and year, and nothing is uploaded.
5. The hard parts
- Provider data is messy and changes. Ids get renamed and ad tiers show up as new providers, so the allowlist needs re-checking by hand.
- Vercel Hobby analytics doesn’t show custom events, so I log a few funnel events to a Supabase table instead.
- Cold start is the real problem, not the code. Pairwise ranking feels good after about 10 comparisons, and getting someone there before they bounce is the whole game.
If you want to poke at it (no signup needed to rank a few titles): https://movie-app-nu-ruby-23.vercel.app/
I’m happy to go deeper on any of this. I’m especially curious whether anyone has a better approach for the provider add-on problem than an allowlist.
Pairwise ranking is definitely better than stars, but nobody wants to read your slop, sorry.
Google shows there are lots of these already: Flickchart, Canonova (slop), moup.app (also slop), popcornlist (possibly slop but they at least wrote their own prose so they get a pass, though it is an iPhone app only), pairckle (generic pairwise ranking, not slop!)
Also it doesn’t sound like you have figured out how to actually do the ranking yet. Look up Bradley-Terry or TrueSkill.
As usually, the difficult bit is getting users.

