24 May 2026
📌 Sunday evening in Gourock, the Firth gone the same flat grey as the sky, Katatonia's Sky Void of Stars low in the headphones, one of the cats asleep across the amrchair. The plan was to do nothing in particular. Instead I spent the evening watching my own YouTube front-end fall over, because somebody in Mountain View renamed a key in a JSON object and a whole class of software stopped working at once. Btw: Chuck Severance started an amazing series about AI coding. Amazing...
the breakage
My Invidious instance lives on a Proxmox CT, part of the small pile of front-ends I keep between me and the platforms - the thing that lets me watch a channel without the channel watching me back. Tonight every channel page threw the same error:
Missing hash key: "collectionThumbnailViewModel" (KeyError)
from /usr/lib/crystal/json/any.cr:149:5 in 'dig'
from src/invidious/yt_backend/extractors.cr:653:30 in 'parse_internal'
from src/invidious/channels/videos.cr:32:12 in 'get_videos:continuation:sort_by'
from src/invidious/routes/channels.cr:11:5 in 'home'
YouTube had changed the shape of its internal Innertube response. Channel videos, which Invidious used to parse directly, are now wrapped in a generic container called a lockupViewModel, which comes in three flavours - LOCKUP_CONTENT_TYPE_VIDEO, LOCKUP_CONTENT_TYPE_PLAYLIST, LOCKUP_CONTENT_TYPE_PODCAST. The parser went looking for a key in the place it had always been, the key wasn't there any more, and [] on a Crystal JSON::Any does exactly what it promises: KeyError, request dead.
It wasn't just me, and it wasn't just Invidious. The same restructure broke FreeTube and NewPipe on the same day. When three independent scrapers fall over within hours of each other, it's never their bug. It's the furniture being moved in a house none of them own.
guest on someone else's web
This is the thing I keep circling back to. A privacy front-end like Invidious doesn't have an API contract with YouTube - there is no contract, because from YouTube's point of view we shouldn't be here at all. The whole edifice stands on reading the same internal JSON the official web client reads, and reverse-engineering its shape. The shape is undocumented and can change on any Tuesday without notice, because it was never promised to anyone. We are guests, and the host is under no obligation to keep the door where it was yesterday.
The remarkable part isn't that it breaks. It's how fast a handful of people put it back together.
the fix that wasn't merged
By the time I'd finished reading the backtrace there was already an issue open (#5737) and, more useful, a fix: PR #5736 by Fijxu, teaching the extractor to walk the new lockupViewModel for the video and playlist cases. One catch - it was still a draft. Not merged to master. Which mattered, because the image I was running is the official prebuilt one:
image: quay.io/invidious/invidious:latest
quay only publishes from master. A docker compose pull would never hand me a fix that lives on a contributor's fork. To run it tonight I had to stop pulling someone else's binary and start building the source myself, pinned to the exact branch.
The channel parsing that breaks lives in Invidious proper - extractors.cr - not in the companion container, which only handles the player and stream side. So companion and Postgres stayed exactly as they were. Only the one service had to change.
the build
Backup the compose, clone the fork next to it, check out the branch:
cp docker-compose.yml docker-compose.yml.bak
git clone https://github.com/Fijxu/invidious.git invidious-src
cd invidious-src && git checkout fix-videos-in-channels-and-playlists-in-channels-and-searches && cd ..
Then the one edit that matters - in the invidious service, the image: line out, a local build in:
build:
context: ./invidious-src
dockerfile: docker/Dockerfile
Everything else - the inline INVIDIOUS_CONFIG, the ports, the healthcheck, the depends_on - left untouched. Build the source, bring it up:
docker compose build --no-cache invidious
docker compose up -d
A few minutes of Crystal compiling later, the container is no longer quay.io/invidious/invidious:latest but my own local image, and the channel pages load. No KeyError. The videos are back.
fixed but not merged
Which leaves the box in a state I've started to recognise as its own genre. Not closed but not patched, like the kernel a few weeks ago - this time fixed but not merged. The thing that protects the instance isn't a release. It's a draft branch, code that nobody has signed off on yet, carrying production until the official version lands clean.
And it carries it imperfectly, on purpose. The draft has known holes, listed right there in the PR: channel verification ticks are gone, because authorVerified is hardcoded to false for now; some video durations read as 0; a few channels come back with empty playlists. None of that is mine to fix tonight, and none of it stops the thing from doing the one job I needed back. It's the usual shape of these moments - the workaround does the actual work, and the proper fix arrives later to make the workaround redundant rather than load-bearing.
So I'm a fork-in-place maintainer of my own little Invidious until #5736 merges. When it does, the rollback is three lines: restore the .bak, docker compose pull, drop the invidious-src directory. The instance goes back to being a stranger's binary, and I go back to not thinking about it.
That's the deal with living between the platforms. The work is never finished, because the platform is actively hostile to the work existing at all. But twenty people on GitHub turned a renamed JSON key back into a working channel page inside a day, and I turned their draft into a running container inside an evening, and somewhere in Mountain View nobody noticed or cared. That asymmetry - them indifferent, us stubborn - is most of what keeps the open web open.
The album finished a while ago and I haven't put another on. Mizar and Alcor hasn't moved. It's late, the channels load, and that's enough for a Sunday.