Ranks videos in a YouTube playlist or channel by engagement metrics, so you can prioritise what to watch — especially useful for large conference playlists.
Pre-built binaries for Linux, macOS and Windows (amd64/arm64) are attached to every GitHub release.
brew install fedir/tap/yrankRecent Homebrew versions require trusting a third-party tap before its formula
will load. If you see Refusing to load formula ... from untrusted tap, run:
brew trust fedir/tap
brew install fedir/tap/yrankUpgrade to the latest release later with:
brew upgrade yrankVerify the install:
yrank -versiongo install github.com/fedir/yrank@latestgit clone https://github.com/fedir/yrank
cd yrank
make buildYou need a YouTube Data API v3 key from Google Developers Console:
- Create a project and enable YouTube Data API v3
- Generate an API key
- Copy
.env.exampleto.envand set your key:
YOUTUBE_API_KEY=your_key_here
The key is also read directly from the environment if set there.
| Flag | Default | Description |
|---|---|---|
-p |
— | YouTube playlist ID |
-c |
— | YouTube channel ID or handle (e.g. @Squeezie) |
-top-search |
— | Search YouTube for a word/phrase and rank the matching videos |
-o |
table |
Output format: table, markdown, or csv |
-out |
— | Write output to file atomically (safer than shell redirection for large exports) |
-in |
— | Filter an existing CSV export locally (no API): read this file, apply the view/duration filters, write the same format to -out |
-check |
— | Validate a yrank CSV export locally (no API) and exit non-zero on failure |
-s |
total-interest |
Sort by metric (see below). Mutually exclusive with -strategy |
-strategy |
— | Score and rank by evaluation strategy (see below). Mutually exclusive with -s |
-weights |
— | Override strategy weights: key=val,key=val |
-from |
— | Only include videos published on or after this date (YYYY-MM-DD) |
-min-length |
0 (no min) |
Only include videos at least N seconds long |
-max-length |
0 (no max) |
Only include videos at most N seconds long |
-min-views |
0 (no min) |
Only include videos with at least N views |
-m |
0 (all) |
Maximum number of results to return |
-local-test |
false |
Use local testdata/ fixtures instead of live API calls (no quota consumed) |
-d |
false |
Debug mode — prints API URLs and IDs |
-version / -V |
— | Print version and exit |
Exactly one input source — -p, -c, or -top-search — must be given; they are mutually exclusive.
-top-search uses the YouTube search.list endpoint, which costs 100 quota units per page of 50 results (vs 1 unit for playlist/channel listing). It paginates up to -m candidates before ranking them; with the default -m 0 it fetches a single page (≤50 videos).
See Quota & limits for the full cost model and the hard caps the YouTube API imposes.
total-interest (default) · positive-interest · likes · total-reaction · global-buzz-index · positive-negative-coefficient · pnc · duration
Every result includes a Duration column in seconds (from the API's contentDetails.duration). -min-length / -max-length keep only videos within the given second bounds (0 = no limit on that side); both apply before sorting. Videos with an unknown duration (0, e.g. live placeholders) are dropped only when -min-length is set.
./yrank -c @Vsauce -min-length 300 # only videos longer than 5 min
./yrank -c @Vsauce -max-length 60 -s duration # Shorts-length clips, longest first-min-views N keeps only videos with at least N views (0 = no limit), applied before sorting alongside the other filters.
./yrank -c @Vsauce -min-views 1000000 # only videos past 1M views
./yrank -c @Vsauce -min-length 300 -min-views 50000Already exported a channel and want a tighter cut without spending quota again? Feed an existing
CSV back in with -in (or make local-filter). It locates the Views/Duration columns by
header and writes the same format — works on both base and -strategy all exports.
./yrank -in sample_output/vsauce_channel_all.csv -out vsauce_long.csv -min-length 900 -min-views 100000
# or via make (IN and OUT required):
make local-filter IN=sample_output/vsauce_channel_all.csv OUT=vsauce_long.csv MIN_VIEWS=100000 MIN_LENGTH=900One command to export a full channel, validate it with Go checks (-check), and
git add/commit/push just that CSV with a predefined message:
make publish-channel CHANNEL=@NASA
# → sample_output/nasa_channel_all.csv, validated, committed as
# "chore: add @NASA full channel export", then pushed.
make publish-channel CHANNEL=@NASA EXPORT_MSG="chore: refresh NASA export" # custom messageThe commit is gated on the checks: if -check finds an empty/short export, a missing
column, a views <= 0 row, or uniform per-video stats, the run stops before committing.
Each strategy scores videos by a weighted formula over raw signals, then sorts by Score. A Score column is prepended to the output.
Use -strategy all to compute all 6 strategies at once — adds one score column per strategy (Score:viral, Score:educational, …) and sorts by total-interest. Ideal for CSV export and cross-strategy comparison.
| Slug | Lens | Weight keys |
|---|---|---|
viral — algo/trending lens |
Rewards videos with high engagement rate relative to their audience size.
score = 0.5 × (likes+dislikes)/views
+ 0.3 × views/max_views ← reach normalised across dataset
+ 0.2 × comments/views
educational — tutorial/reference lens
Rewards likes and discussion; penalises recency so old-but-solid content ranks high.
score = 0.6 × likes/views
+ 0.3 × comments/views
+ 0.1 × 1/age_days
controversial — debate/polarising lens
Rewards a high dislike ratio multiplied by total reaction volume (log-scaled to reduce outliers).
score = ratio × volume
ratio = (dislikes+1) / (likes+1)
volume = log(likes+dislikes+1)
community — fan/community-building lens
Comments are the primary signal; positive sentiment is secondary.
score = 0.5 × comments/views
+ 0.5 × norm_sentiment
norm_sentiment = pnc / (1 + pnc), pnc = likes/(1+dislikes)
evergreen — long-tail/SEO lens
Rewards videos that accumulate steady engagement per day since publication.
score = 0.5 × (likes+comments)/age_days
+ 0.5 × 1/age_days
hype — launch/premiere lens
Pure view velocity: views per day since publication.
score = views / age_days
age_days = days since PublishedAt (min 1). max_views = highest view count in the dataset (for viral normalisation).
| Slug | Weight keys |
|---|---|
viral |
engagement, reach, comments |
educational |
likes, comments, recency |
controversial |
ratio, volume |
community |
comments, sentiment |
evergreen |
engagement, age |
hype |
velocity |
Weight override priority (highest wins):
- Strategy defaults (hardcoded in
youtube/strategy.go) .envvariables:WEIGHT_<STRATEGY>_<KEY>=0.7— e.g.WEIGHT_VIRAL_ENGAGEMENT=0.7-weightsCLI flag:key=val,key=val— e.g.-weights engagement=0.9,reach=0.05,comments=0.05
# Rank a playlist by total interest (default)
./yrank -p PLAYLIST_ID
# Rank in markdown, sorted by positive interest, top 10
./yrank -p PLAYLIST_ID -o markdown -s positive-interest -m 10
# Rank a whole channel using a handle
./yrank -c @Squeezie -s positive-interest -o markdown
# Search YouTube for a phrase and rank the top 20 matches by viral score
./yrank -top-search "kubernetes operator" -strategy viral -m 20
# Only videos from 2025 onwards
./yrank -c @Squeezie -from 2025-01-01 -s positive-interest
# Rank by viral strategy
./yrank -p PLAYLIST_ID -strategy viral
# Rank by viral strategy with custom weights
./yrank -p PLAYLIST_ID -strategy viral -weights engagement=0.9,reach=0.05,comments=0.05
# Export to CSV (pipe-safe, emojis and special chars handled correctly)
./yrank -c @TiboInShape -o csv -out tiboinshape.csv
# Score with all strategies at once (6 score columns)
./yrank -p PLAYLIST_ID -strategy all -o csv -out all_strategies.csv
# Use local fixtures — no API quota consumed
./yrank -p PLiVdPopzGBsV7TgjAw9GH43Ck9QCxrw5w -local-test
./yrank -p PLiVdPopzGBsV7TgjAw9GH43Ck9QCxrw5w -local-test -strategy all -o csv
# Save results to a file atomically (preferred over shell redirection)
./yrank -p PLAYLIST_ID -strategy evergreen -o markdown -out results.mdyrank talks to the YouTube Data API v3, which is metered in quota units (the API's "tokens"), not in number of requests. The default project budget is 10,000 units/day, resetting at midnight US-Pacific time. This chapter is the cost model and the strategy yrank follows to stay inside it.
| Endpoint | Used for | Cost |
|---|---|---|
playlistItems.list |
listing a playlist / channel uploads (≤50 items/page) | 1 unit/page |
videos.list |
per-video statistics + duration (up to 50 IDs/call) | 1 unit/call |
channels.list |
resolving @handle, listing manual playlists |
1 unit |
search.list |
-top-search (≤50 results/page) |
100 units/page |
- Batch
videos.listto 50 IDs per call. Statistics are the dominant cost, and a call costs 1 unit whether it carries 1 ID or 50. We collect IDs from the listing, then fetch stats in chunks of 50 — roughly a 50× reduction versus one call per video. - Listing carries only what's free. Pagination collects
id+title+publishedAtonly; everything else (views, likes, comments, duration) comes from the batchedvideos.list, joined back by ID. - Deduplicate IDs before fetching stats. For a channel, IDs from the uploads playlist and
manual playlists are deduplicated before any
videos.listcall, so a video in several playlists is paid for once. - Filters are client-side and free.
-min-views/-min-length/-fromcannot be pushed to the API (it has no such parameters), but they run on already-fetched data, so they add zero quota.
Rule of thumb: a channel/playlist of N videos costs about 2 × ceil(N/50) units
(listing + stats). Examples: Naval Group (239 videos) ≈ 10 units; a 20k-video channel
≈ ~1,200 units. A -top-search run adds 100 units per 50 candidates.
- ~20,000-video cap per channel.
playlistItems.liststops paginating a channel's uploads playlist at ~20k items. Channels larger than that cannot be fully exported with-c(e.g. CCTV Video News Agency has 43,634 videos but only ~20,048 are retrievable this way). search.listcaps at ~500 results per query (pagination limit), at 100 units/page.- No server-side filtering by view count or precise duration — the data must be fetched
before any
-min-views/-min-lengthfilter can apply. dislikeCountis gone. YouTube removed public dislikes from the API in December 2021, so that field is always0and the dislike-based metrics effectively run withdislikes = 0.- No resume on interruption (yet). If the daily quota is exhausted or a rate limit hits mid-run, the run aborts and partial work is lost. A resumable/queued mode is planned.
The exact remaining number is only visible in the Google Cloud Console (APIs & Services → YouTube Data API v3 → Quotas). With just an API key you can probe whether quota is still available with a 1-unit call:
curl -s -o /dev/null -w '%{http_code}\n' \
"https://www.googleapis.com/youtube/v3/videos?part=id&id=NCU_Sebq6Tw&key=$YOUTUBE_API_KEY"
# 200 = quota available · 403 = quota exhausted / rate-limitedConferences & playlists:
- KubeCon + CloudNativeCon Europe 2026 · all strategies (CSV)
- CNCF Observability Day Europe 2026
- Cloud Native Days France 2026 (CSV)
- GrafanaCon — all strategies (CSV)
- FOSDEM 2020
- Squeezie — Concepts originaux
Full-channel exports (all strategies, CSV):
- Squeezie · Vsauce · HugoDécrypte Actus · Mister V · Tibo InShape
- Airbus Defence and Space · Naval Group · INA Histoire
Filtered exports (duration filters):
Releases are fully automated with GoReleaser. Pushing a
v* tag triggers the release workflow, which
cross-compiles binaries (linux/darwin/windows × amd64/arm64), publishes a GitHub
release with checksums and a changelog, and updates the Homebrew tap.
git tag v1.2.3
git push origin v1.2.3Build a local snapshot to test the release config without publishing:
make snapshot # requires goreleaser installed locallyThe Homebrew formula update requires a HOMEBREW_TAP_TOKEN repository secret with
write access to fedir/homebrew-tap.