Spaces:
Sleeping
Sleeping
Commit ·
29c4ada
1
Parent(s): dc57844
Add PO Token Grabber browser extension (one-click visitor data + gvs PO token)
Browse filesChrome/Edge MV3 extension under extension/: a read-only webRequest listener captures the
gvs `pot` from googlevideo videoplayback requests, and the popup reads `visitor_data` from
the active YouTube tab's ytcfg (MAIN-world scripting). Formats the PO token as
`web.gvs+<token>` for one-click copy into the Space's PO-token / visitor-data fields.
README points users to it as the easy alternative to manual DevTools extraction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README.md +9 -0
- extension/README.md +50 -0
- extension/background.js +32 -0
- extension/manifest.json +13 -0
- extension/popup.html +44 -0
- extension/popup.js +100 -0
README.md
CHANGED
|
@@ -68,6 +68,15 @@ start failing.
|
|
| 68 |
Paste the values into the app's **"YouTube access — cookies / proxy"** panel:
|
| 69 |
**PO token** field and **Visitor data** field.
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
### Get them manually (web client, ~2 min)
|
| 72 |
|
| 73 |
1. Open a fresh **Incognito** window and go to <https://www.youtube.com> (a throwaway
|
|
|
|
| 68 |
Paste the values into the app's **"YouTube access — cookies / proxy"** panel:
|
| 69 |
**PO token** field and **Visitor data** field.
|
| 70 |
|
| 71 |
+
### Easiest: the PO Token Grabber extension (one click)
|
| 72 |
+
|
| 73 |
+
This repo ships a tiny Chrome/Edge extension in [`extension/`](extension/) that reads the
|
| 74 |
+
PO token + visitor data for you. Download that folder, load it unpacked
|
| 75 |
+
(`chrome://extensions` → Developer mode → Load unpacked), open a YouTube video, press
|
| 76 |
+
play, click the extension, and **Copy** each value into the Space. See
|
| 77 |
+
[`extension/README.md`](extension/README.md). (It's unpublished/sideloaded and may need
|
| 78 |
+
updates when YouTube changes; the manual steps below always work as a fallback.)
|
| 79 |
+
|
| 80 |
### Get them manually (web client, ~2 min)
|
| 81 |
|
| 82 |
1. Open a fresh **Incognito** window and go to <https://www.youtube.com> (a throwaway
|
extension/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TutorialMaker — PO Token Grabber (browser extension)
|
| 2 |
+
|
| 3 |
+
A tiny **Chrome/Edge (Manifest V3)** extension that reads the two values the
|
| 4 |
+
[TutorialMaker Space](https://huggingface.co/spaces/vivekchakraverty/TutorialMaker) needs
|
| 5 |
+
to get past YouTube's bot-check **without a proxy**:
|
| 6 |
+
|
| 7 |
+
- **`visitor_data`** — read from the YouTube page's `ytcfg`.
|
| 8 |
+
- **gvs `pot`** (Proof-of-Origin token) — captured from the player's
|
| 9 |
+
`googlevideo.com/videoplayback` request.
|
| 10 |
+
|
| 11 |
+
It just surfaces these for one-click copy; it sends nothing anywhere.
|
| 12 |
+
|
| 13 |
+
## Install (unpacked)
|
| 14 |
+
|
| 15 |
+
1. Open `chrome://extensions` (or `edge://extensions`).
|
| 16 |
+
2. Turn on **Developer mode** (top-right).
|
| 17 |
+
3. Click **Load unpacked** and select this `extension/` folder.
|
| 18 |
+
|
| 19 |
+
> Firefox: it's MV3-compatible in spirit, but Firefox needs a `background.scripts` entry
|
| 20 |
+
> and a `browser_specific_settings` block instead of `service_worker`. Minor manifest
|
| 21 |
+
> tweaks required.
|
| 22 |
+
|
| 23 |
+
## Use
|
| 24 |
+
|
| 25 |
+
1. **Use a throwaway Google account**, not your main one.
|
| 26 |
+
2. Open a YouTube **video** and press **play** (this makes the player fetch
|
| 27 |
+
`videoplayback`, which carries the `pot`).
|
| 28 |
+
3. Click the extension icon.
|
| 29 |
+
4. **Copy PO token** → paste into the Space's **"PO token"** field.
|
| 30 |
+
**Copy visitor data** → paste into the Space's **"Visitor data"** field.
|
| 31 |
+
5. Run the Space. Tokens expire within hours — re-grab when downloads start failing.
|
| 32 |
+
|
| 33 |
+
## How it works
|
| 34 |
+
|
| 35 |
+
- `background.js` — a read-only `webRequest` listener on `*://*.googlevideo.com/videoplayback*`
|
| 36 |
+
pulls the `pot` (and `c`/client) query params and stores the latest in
|
| 37 |
+
`chrome.storage.session`. It prefers `c=WEB` tokens (what yt-dlp's web client uses).
|
| 38 |
+
- `popup.js` — on open, runs a MAIN-world script via `chrome.scripting` on the active
|
| 39 |
+
YouTube tab to read `visitor_data`, reads the captured `pot`, and formats the PO token
|
| 40 |
+
as `web.gvs+<token>`.
|
| 41 |
+
|
| 42 |
+
## Caveats
|
| 43 |
+
|
| 44 |
+
- **Unpublished / sideloaded** — you load it yourself; trust accordingly. The code is
|
| 45 |
+
small and dependency-free on purpose.
|
| 46 |
+
- **YouTube changes break it** — the `pot`/`visitor_data` locations move over time; this
|
| 47 |
+
may need updates. The authoritative manual method lives in the
|
| 48 |
+
[yt-dlp PO Token Guide](https://github.com/yt-dlp/yt-dlp/wiki/PO-Token-Guide).
|
| 49 |
+
- **Per session** — tokens are short-lived; this is a one-click refresh, not a permanent fix.
|
| 50 |
+
- Automating bot-check bypass is the same YouTube-ToS gray area as the rest of this tooling.
|
extension/background.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Service worker: watch the player's videoplayback requests and capture the gvs PO token.
|
| 2 |
+
//
|
| 3 |
+
// YouTube's web player fetches video data from *.googlevideo.com/videoplayback?...&pot=...
|
| 4 |
+
// where `pot` is the gvs Proof-of-Origin token and `c` is the client (e.g. WEB). We read
|
| 5 |
+
// it (read-only; no blocking) and stash the latest into session storage for the popup.
|
| 6 |
+
|
| 7 |
+
const FILTER = { urls: ["*://*.googlevideo.com/videoplayback*"] };
|
| 8 |
+
|
| 9 |
+
chrome.webRequest.onBeforeRequest.addListener(
|
| 10 |
+
(details) => {
|
| 11 |
+
try {
|
| 12 |
+
const u = new URL(details.url);
|
| 13 |
+
const pot = u.searchParams.get("pot");
|
| 14 |
+
if (!pot) return;
|
| 15 |
+
const client = u.searchParams.get("c") || "";
|
| 16 |
+
// Prefer WEB tokens (what yt-dlp's web client needs); still record others as backup.
|
| 17 |
+
chrome.storage.session.get(["gvsPot", "gvsClient"]).then((cur) => {
|
| 18 |
+
const haveWeb = cur.gvsClient === "WEB";
|
| 19 |
+
if (!haveWeb || client === "WEB") {
|
| 20 |
+
chrome.storage.session.set({
|
| 21 |
+
gvsPot: pot,
|
| 22 |
+
gvsClient: client,
|
| 23 |
+
potTime: Date.now(),
|
| 24 |
+
});
|
| 25 |
+
}
|
| 26 |
+
});
|
| 27 |
+
} catch (e) {
|
| 28 |
+
// ignore malformed URLs
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
FILTER
|
| 32 |
+
);
|
extension/manifest.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"manifest_version": 3,
|
| 3 |
+
"name": "TutorialMaker PO Token Grabber",
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"description": "Reads your YouTube visitor data and gvs PO token so you can paste them into the TutorialMaker Space.",
|
| 6 |
+
"permissions": ["webRequest", "storage", "scripting", "tabs"],
|
| 7 |
+
"host_permissions": ["*://*.youtube.com/*", "*://*.googlevideo.com/*"],
|
| 8 |
+
"background": { "service_worker": "background.js" },
|
| 9 |
+
"action": {
|
| 10 |
+
"default_popup": "popup.html",
|
| 11 |
+
"default_title": "PO Token Grabber"
|
| 12 |
+
}
|
| 13 |
+
}
|
extension/popup.html
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<style>
|
| 6 |
+
body { font-family: system-ui, sans-serif; width: 340px; margin: 0; padding: 12px; }
|
| 7 |
+
h1 { font-size: 14px; margin: 0 0 8px; }
|
| 8 |
+
label { display: block; font-size: 11px; color: #444; margin: 8px 0 2px; }
|
| 9 |
+
textarea { width: 100%; box-sizing: border-box; font-family: monospace; font-size: 11px;
|
| 10 |
+
resize: vertical; }
|
| 11 |
+
.row { display: flex; gap: 6px; margin-top: 6px; }
|
| 12 |
+
button { flex: 1; padding: 6px; font-size: 12px; cursor: pointer; }
|
| 13 |
+
#status { margin-top: 10px; font-size: 11px; min-height: 14px; }
|
| 14 |
+
#status.ok { color: #1a7f37; }
|
| 15 |
+
#status.warn { color: #9a6700; }
|
| 16 |
+
.hint { font-size: 10px; color: #666; margin-top: 8px; line-height: 1.4; }
|
| 17 |
+
</style>
|
| 18 |
+
</head>
|
| 19 |
+
<body>
|
| 20 |
+
<h1>PO Token Grabber</h1>
|
| 21 |
+
|
| 22 |
+
<label for="potField">PO token (paste into the Space's "PO token" field)</label>
|
| 23 |
+
<textarea id="potField" rows="2" readonly placeholder="web.gvs+…"></textarea>
|
| 24 |
+
|
| 25 |
+
<label for="visField">Visitor data (paste into the Space's "Visitor data" field)</label>
|
| 26 |
+
<textarea id="visField" rows="2" readonly placeholder="Cgt…%3D%3D"></textarea>
|
| 27 |
+
|
| 28 |
+
<div class="row">
|
| 29 |
+
<button id="copyPot">Copy PO token</button>
|
| 30 |
+
<button id="copyVis">Copy visitor data</button>
|
| 31 |
+
</div>
|
| 32 |
+
<div class="row">
|
| 33 |
+
<button id="refresh">Refresh</button>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<div id="status"></div>
|
| 37 |
+
<div class="hint">
|
| 38 |
+
Tip: open a YouTube video, press <b>play</b> (so the token is generated), then click
|
| 39 |
+
this icon. Use a throwaway account. Tokens expire within hours.
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<script src="popup.js"></script>
|
| 43 |
+
</body>
|
| 44 |
+
</html>
|
extension/popup.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Popup: read visitor_data from the active YouTube tab (main world) and the captured gvs
|
| 2 |
+
// PO token from session storage, then offer ready-to-paste values for the Space.
|
| 3 |
+
|
| 4 |
+
// Runs in the page's MAIN world via chrome.scripting; must be self-contained.
|
| 5 |
+
function readVisitorData() {
|
| 6 |
+
try {
|
| 7 |
+
if (window.ytcfg && typeof window.ytcfg.get === "function") {
|
| 8 |
+
const v = window.ytcfg.get("VISITOR_DATA");
|
| 9 |
+
if (v) return v;
|
| 10 |
+
const ctx = window.ytcfg.get("INNERTUBE_CONTEXT");
|
| 11 |
+
if (ctx && ctx.client && ctx.client.visitorData) return ctx.client.visitorData;
|
| 12 |
+
}
|
| 13 |
+
} catch (e) {}
|
| 14 |
+
try {
|
| 15 |
+
const r = window.ytInitialPlayerResponse;
|
| 16 |
+
if (r && r.responseContext && r.responseContext.visitorData) {
|
| 17 |
+
return r.responseContext.visitorData;
|
| 18 |
+
}
|
| 19 |
+
} catch (e) {}
|
| 20 |
+
return null;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
function setStatus(msg, kind) {
|
| 24 |
+
const el = document.getElementById("status");
|
| 25 |
+
el.textContent = msg;
|
| 26 |
+
el.className = kind || "";
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
async function getActiveTab() {
|
| 30 |
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
| 31 |
+
return tab;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function isYouTube(url) {
|
| 35 |
+
return /^https?:\/\/([^/]*\.)?youtube\.com\//.test(url || "");
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
async function load() {
|
| 39 |
+
const potEl = document.getElementById("potField");
|
| 40 |
+
const visEl = document.getElementById("visField");
|
| 41 |
+
|
| 42 |
+
const tab = await getActiveTab();
|
| 43 |
+
let visitor = null;
|
| 44 |
+
if (tab && isYouTube(tab.url)) {
|
| 45 |
+
try {
|
| 46 |
+
const res = await chrome.scripting.executeScript({
|
| 47 |
+
target: { tabId: tab.id },
|
| 48 |
+
world: "MAIN",
|
| 49 |
+
func: readVisitorData,
|
| 50 |
+
});
|
| 51 |
+
visitor = res && res[0] ? res[0].result : null;
|
| 52 |
+
} catch (e) {}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const { gvsPot, gvsClient, potTime } = await chrome.storage.session.get([
|
| 56 |
+
"gvsPot",
|
| 57 |
+
"gvsClient",
|
| 58 |
+
"potTime",
|
| 59 |
+
]);
|
| 60 |
+
|
| 61 |
+
visEl.value = visitor || "";
|
| 62 |
+
potEl.value = gvsPot ? "web.gvs+" + gvsPot : "";
|
| 63 |
+
|
| 64 |
+
const notes = [];
|
| 65 |
+
if (!isYouTube(tab && tab.url)) {
|
| 66 |
+
notes.push("Open a YouTube video tab, then reopen this popup.");
|
| 67 |
+
} else {
|
| 68 |
+
if (!visitor) notes.push("Couldn't read visitor data — refresh the video page.");
|
| 69 |
+
if (!gvsPot) notes.push("No PO token yet — press play on a video, then reopen.");
|
| 70 |
+
}
|
| 71 |
+
if (gvsPot && gvsClient && gvsClient !== "WEB") {
|
| 72 |
+
notes.push("Captured client is '" + gvsClient + "', not WEB — play in the normal web player.");
|
| 73 |
+
}
|
| 74 |
+
if (potTime) {
|
| 75 |
+
const mins = Math.round((Date.now() - potTime) / 60000);
|
| 76 |
+
notes.push("Token captured ~" + mins + " min ago (they expire within hours).");
|
| 77 |
+
}
|
| 78 |
+
setStatus(notes.join(" ") || "Ready — copy the values into the Space.", notes.length ? "warn" : "ok");
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
async function copy(id, label) {
|
| 82 |
+
const val = document.getElementById(id).value;
|
| 83 |
+
if (!val) {
|
| 84 |
+
setStatus("Nothing to copy yet for " + label + ".", "warn");
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
try {
|
| 88 |
+
await navigator.clipboard.writeText(val);
|
| 89 |
+
setStatus("Copied " + label + " ✓", "ok");
|
| 90 |
+
} catch (e) {
|
| 91 |
+
setStatus("Copy failed — select the text and copy manually.", "warn");
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 96 |
+
load();
|
| 97 |
+
document.getElementById("copyPot").addEventListener("click", () => copy("potField", "PO token"));
|
| 98 |
+
document.getElementById("copyVis").addEventListener("click", () => copy("visField", "visitor data"));
|
| 99 |
+
document.getElementById("refresh").addEventListener("click", load);
|
| 100 |
+
});
|