From 7589c092011b6e651b3278f3901c2147c3372cb2 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 3 Jun 2026 09:35:36 +0200 Subject: [PATCH] fix(youtube): use a browser UA for the watch-page caption scrape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ANDROID InnerTube player now returns 0 captionTracks (PoToken-gated), so the watch-page scrape is the real path — but it sent an Android *app* User-Agent, which makes YouTube serve a page WITHOUT ytInitialPlayerResponse, so captions came back empty. Result: 87% of a live run skipped as 'no transcript' despite the videos having captions. A desktop-browser UA returns the player JSON with captionTracks. Confirmed live: app-UA=0 tracks, browser-UA=1. --- internal/adapters/youtube/captions.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/adapters/youtube/captions.go b/internal/adapters/youtube/captions.go index 5797bdc..de87d88 100644 --- a/internal/adapters/youtube/captions.go +++ b/internal/adapters/youtube/captions.go @@ -21,6 +21,11 @@ const defaultPlayerBaseURL = "https://www.youtube.com" // returns caption baseUrls without a PoToken requirement (ADR-010). const androidUserAgent = "com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip" +// browserUserAgent is required for the watch-page scrape: with an app UA YouTube +// serves a page WITHOUT the embedded ytInitialPlayerResponse, so captionTracks +// come back empty. A desktop-browser UA returns the player JSON with captions. +const browserUserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" + // maxCaptionBytes bounds a single timedtext/player body read. const maxCaptionBytes = 16 << 20 // 16 MiB @@ -134,7 +139,7 @@ const ytInitialMarker = "ytInitialPlayerResponse" // embedded ytInitialPlayerResponse JSON. Any failure degrades to no tracks. func (a *Adapter) scrapeCaptionTracks(ctx context.Context, client *http.Client, videoID string) ([]captionTrack, error) { body, status, err := a.httpGet(ctx, client, a.playerBaseURL+"/watch?v="+videoID, map[string]string{ - "User-Agent": androidUserAgent, + "User-Agent": browserUserAgent, }) if err != nil { return nil, err