Skip to content

feat(video): share-to-video web route + extension YouTube ingestion#1156

Merged
mircealungu merged 1 commit into
masterfrom
feat/share-to-video-client
Jun 1, 2026
Merged

feat(video): share-to-video web route + extension YouTube ingestion#1156
mircealungu merged 1 commit into
masterfrom
feat/share-to-video-client

Conversation

@mircealungu
Copy link
Copy Markdown
Member

Client-side half of the share-to-video flow. Pairs with zeeguu/api#635 (already merged + deployed).

End-to-end smoke-tested in prod today: the API side already accepts uploads from the new extension (server log shows POST /video_upload/create → 200 with the captioned video row + topic inferred). This PR adds the missing /shared-video web route that the extension opens on success — the only piece preventing the share flow from completing cleanly.

What happens when a user clicks the Zeeguu icon on a YouTube watch page

  1. isYouTubeTab(tab) matches the URL.
  2. The shared helper shareYouTubeTab(api, tab, WEB_URL) is called from both the popup (toolbar click) and background.js (right-click "Read with Zeeguu").
  3. chrome.scripting.executeScript({ world: \"MAIN\" }) runs an extractor that reads window.__zeeguuCapturedCaptions[videoId:lang] — populated by the monkey-patch (see below).
  4. Selected track is POSTed to /video_upload/create along with video_unique_key and language.
  5. Extension opens ${WEB_URL}/shared-video?video_id=<id>.
  6. SharedVideoHandler fetches video info, shows "Preparing <title>…" for ~800ms so the user can read the title, then redirects to /watch/video?id=<id> (existing route → existing VideoPlayer + the TranslateCaptionsControl from feat(video): translated subtitles control on shared-video reader (v1.5) #1155).

Why a monkey-patch

YouTube's `/api/timedtext` baseUrl from playerCaptionsTracklistRenderer now requires a one-shot PO Token (Proof of Origin Token) computed by YouTube's BotGuard JS. We cannot reproduce it; direct fetches of the baseUrl (with or without fmt=srv1) return empty bodies or HTTP 404. This is the same anti-scraping that yt-dlp and youtube_transcript_api fight quarterly.

The workaround: capture the player's own successful fetch.

  • youtubeContentScript.js runs at `document_start` on `*.youtube.com` / `youtu.be` (ISOLATED world) and injects `youtubePatch.js` via `<script src=chrome.runtime.getURL(...)>` (declared in `web_accessible_resources` so YouTube's CSP accepts it).
  • `youtubePatch.js` runs in MAIN world. It wraps `window.fetch` and `XMLHttpRequest.{open,send}`, watches for `/api/timedtext` URLs, parses the response (JSON3 default, srv1 XML fallback), and stashes each result on `window.__zeeguuCapturedCaptions[videoId:lang]`.
  • When the user enables CC, YouTube's player fetches captions, the patch captures the response, and the next Zeeguu-icon click succeeds.

When nothing has been captured yet, the popup surfaces a clear prompt: "Please turn on subtitles in the YouTube player (CC button) for this video, then click Zeeguu again."

Files

  • `src/MainAppRouter.js` + `src/reader/SharedVideoHandler.js`: new `/shared-video` route.
  • `src/api/userVideos.js`: `createVideoUpload` (JSON body via `_postJSON` — the captions array doesn't fit form-encoded).
  • `src/extension/src/youtube/youtubePatch.js`: MAIN-world fetch/XHR wraps + caption parsing.
  • `src/extension/src/youtube/youtubeContentScript.js`: injects the patch.
  • `src/extension/src/shared/sendYouTubeTabToZeeguu.js`: orchestrator + `isYouTubeTab`. Track selection: prefer manual over auto-generated within the user's `learned_language`; error out cleanly if no captions in the target language exist.
  • `src/extension/src/shared/shareYouTubeTab.js`: shared entry-point for popup + background.
  • `src/extension/manifest.chrome.dev.json`: declares the new `content_script` + `web_accessible_resource`.
  • `package.json`: `ext:buildDev` now copies the two `youtube/*` files into `build/`.

Why `chrome.scripting` returns the data synchronously

In MV3, `chrome.scripting.executeScript({ world: "MAIN" })` does not reliably await async returns across the world boundary — async functions come back with `result: null`. The extractor is therefore synchronous: it only reads from `window.__zeeguuCapturedCaptions` (already populated by the patch) and returns immediately. No fetches happen across the boundary.

Known caveats

  • Production manifest (`manifest.chrome.json`) NOT updated in this PR — only `manifest.chrome.dev.json`. Production manifest needs the same additions before publishing to the Chrome store.
  • Firefox manifests not updated (this PR is desktop-Chrome-first).
  • iOS share extension would still go down the article path; YouTube-share on iOS is a separate follow-up.

Test plan

  • Reload extension at `chrome://extensions` from the new build (`web-v635/src/extension/build`).
  • Open a YouTube watch page in the user's learned-language; enable CC.
  • Click Zeeguu icon → expect `/shared-video?video_id=N` to load "Preparing <title>..." then redirect to `/watch/video?id=N`.
  • Click Zeeguu icon on a YouTube page where CC has not been enabled → expect the popup to prompt for CC.
  • Click Zeeguu icon on a YouTube page with no captions in target language → expect "This video has no captions in your learning language (XX). Available: YY".
  • Click Zeeguu icon on a non-YouTube page → existing article flow still works.

🤖 Generated with Claude Code

Client-side half of the share-to-video flow paired with zeeguu/api#635.

When the user clicks the Zeeguu icon on a YouTube watch page, the extension
extracts the caption track on the user's residential IP, POSTs it to
/video_upload/create, and opens /shared-video?video_id=<id>, which shows a
brief "Preparing <title>..." screen before redirecting to /watch/video.

YouTube anti-scraping note: the /api/timedtext baseUrl from
playerCaptionsTracklistRenderer now requires a one-shot PO Token (Proof of
Origin Token) that we can't reproduce. Direct fetches return empty bodies or
HTTP 404 even on a fresh page. The fix is a monkey-patch (youtubePatch.js)
injected at document_start into youtube.com tabs via a content script: it
wraps fetch and XMLHttpRequest on the page's MAIN world and stores every
successful /api/timedtext response on window.__zeeguuCapturedCaptions, keyed
by ${videoId}:${langCode}. The share flow then reads from that store. If
nothing has been captured yet, the popup prompts the user to enable CC
(which triggers the player's own fetch that the patch then intercepts).

Pieces:
- src/MainAppRouter.js + src/reader/SharedVideoHandler.js: new /shared-video
  route. Reads ?video_id=, fetches video info, shows the "Preparing <title>..."
  screen for ~800ms so the title is actually visible, then redirects to the
  existing /watch/video?id= route (which already has VideoPlayer +
  TranslateCaptionsControl from #1155).
- src/api/userVideos.js: createVideoUpload (JSON body via _postJSON) -- the
  captions array is awkward as form-encoded data.
- src/extension/src/youtube/youtubePatch.js: MAIN-world fetch/XHR wraps that
  capture timedtext responses, parses JSON3 (the default since 2021) and
  srv1 XML, scoped per (videoId, langCode) so SPA-nav between videos
  doesn't confuse captures.
- src/extension/src/youtube/youtubeContentScript.js: injects youtubePatch.js
  via <script src=chrome.runtime.getURL(...)> so YouTube's CSP doesn't
  reject an inline script.
- src/extension/src/shared/sendYouTubeTabToZeeguu.js: orchestrator. Reads
  captured captions via chrome.scripting.executeScript({ world: "MAIN" })
  (synchronous return only -- chrome.scripting does not reliably await
  async returns across the MAIN-world boundary in MV3). Selects the track
  for the user's learned_language, preferring manual over auto-generated;
  surfaces a clear error when the only available captions are in a
  different language.
- src/extension/src/shared/shareYouTubeTab.js: shared entry-point used by
  both the popup (toolbar click) and background.js (right-click "Read with
  Zeeguu") -- removes the duplicated branch.
- src/extension/manifest.chrome.dev.json: declares the new content_script
  for *.youtube.com / youtu.be at document_start, plus youtubePatch.js as
  a web_accessible_resource.
- package.json: ext:buildDev now copies the two youtube/* JS files into
  build/ (and mkdir -p as a small robustness guard).

Caveat: this is the same anti-scraping cat-and-mouse that yt-dlp and
youtube_transcript_api fight quarterly. The monkey-patch approach
sidesteps it cleanly today (YouTube cannot block us reading data the
player itself just received), but expect to revisit when YouTube changes
the player's caption-fetch path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented Jun 1, 2026

Deploy Preview for voluble-nougat-015dd1 ready!

Name Link
🔨 Latest commit 1ca5199
🔍 Latest deploy log https://app.netlify.com/projects/voluble-nougat-015dd1/deploys/6a1de8b8d47cf00008b00370
😎 Deploy Preview https://deploy-preview-1156--voluble-nougat-015dd1.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@mircealungu mircealungu merged commit d1dc5cb into master Jun 1, 2026
4 checks passed
@mircealungu mircealungu deleted the feat/share-to-video-client branch June 1, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant