Spaces:
Sleeping
Sleeping
Commit Β·
4b8e533
1
Parent(s): 29c4ada
Extension: auto-fill the Space's PO-token / visitor-data fields
Browse files- app.py: stable elem_ids (tm_pot_token, tm_visitor_data) on the Gradio inputs
- extension: "Fill the Space tab" injects values into the Space (all frames, so it works
at *.hf.space and embedded in huggingface.co/spaces) using the native value setter +
input/change events so Gradio's Svelte bindings update; visitor_data cached in session
storage so fill works when the Space tab is focused; add *://*.hf.space/* host permission
- READMEs updated with the auto-fill flow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README.md +4 -2
- app.py +2 -0
- extension/README.md +16 -6
- extension/manifest.json +5 -1
- extension/popup.html +3 -2
- extension/popup.js +87 -16
README.md
CHANGED
|
@@ -72,8 +72,10 @@ Paste the values into the app's **"YouTube access β cookies / proxy"** panel:
|
|
| 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
|
| 76 |
-
play, click the extension
|
|
|
|
|
|
|
| 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 |
|
|
|
|
| 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 and press
|
| 76 |
+
play, then click the extension **while on the YouTube tab** to capture the values. Open
|
| 77 |
+
this Space in another tab and hit **‡ Fill the Space tab** to inject them straight into
|
| 78 |
+
the fields below (or use the Copy buttons and paste). See
|
| 79 |
[`extension/README.md`](extension/README.md). (It's unpublished/sideloaded and may need
|
| 80 |
updates when YouTube changes; the manual steps below always work as a fallback.)
|
| 81 |
|
app.py
CHANGED
|
@@ -297,9 +297,11 @@ def build_ui():
|
|
| 297 |
with gr.Row():
|
| 298 |
po_token_in = gr.Textbox(
|
| 299 |
label="PO token (see README) β CLIENT.CONTEXT+TOKEN", lines=2, scale=3,
|
|
|
|
| 300 |
placeholder="web.gvs+AbCβ¦, web.player+XyZβ¦")
|
| 301 |
visitor_data_in = gr.Textbox(
|
| 302 |
label="Visitor data (pairs with the PO token)", scale=2,
|
|
|
|
| 303 |
placeholder="Cgt...%3D%3D")
|
| 304 |
proxy_url = gr.Textbox(
|
| 305 |
label="Proxy URL (optional)", type="password",
|
|
|
|
| 297 |
with gr.Row():
|
| 298 |
po_token_in = gr.Textbox(
|
| 299 |
label="PO token (see README) β CLIENT.CONTEXT+TOKEN", lines=2, scale=3,
|
| 300 |
+
elem_id="tm_pot_token",
|
| 301 |
placeholder="web.gvs+AbCβ¦, web.player+XyZβ¦")
|
| 302 |
visitor_data_in = gr.Textbox(
|
| 303 |
label="Visitor data (pairs with the PO token)", scale=2,
|
| 304 |
+
elem_id="tm_visitor_data",
|
| 305 |
placeholder="Cgt...%3D%3D")
|
| 306 |
proxy_url = gr.Textbox(
|
| 307 |
label="Proxy URL (optional)", type="password",
|
extension/README.md
CHANGED
|
@@ -25,10 +25,15 @@ It just surfaces these for one-click copy; it sends nothing anywhere.
|
|
| 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 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
## How it works
|
| 34 |
|
|
@@ -36,8 +41,13 @@ It just surfaces these for one-click copy; it sends nothing anywhere.
|
|
| 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`
|
| 40 |
-
as `web.gvs+<token>`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
## Caveats
|
| 43 |
|
|
|
|
| 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 **while on the YouTube tab** β this captures the PO token and
|
| 29 |
+
reads your visitor data.
|
| 30 |
+
4. Open the **TutorialMaker Space** in another tab (the direct `https://<space>.hf.space`
|
| 31 |
+
app URL is the most reliable target).
|
| 32 |
+
5. Click the extension icon again, then **‡ Fill the Space tab** β it injects both values
|
| 33 |
+
straight into the Space's "PO token" and "Visitor data" fields (expand the "YouTube
|
| 34 |
+
access" panel to see them).
|
| 35 |
+
- Or use **Copy PO token** / **Copy visitor data** and paste manually.
|
| 36 |
+
6. Run the Space. Tokens expire within hours β re-grab when downloads start failing.
|
| 37 |
|
| 38 |
## How it works
|
| 39 |
|
|
|
|
| 41 |
pulls the `pot` (and `c`/client) query params and stores the latest in
|
| 42 |
`chrome.storage.session`. It prefers `c=WEB` tokens (what yt-dlp's web client uses).
|
| 43 |
- `popup.js` β on open, runs a MAIN-world script via `chrome.scripting` on the active
|
| 44 |
+
YouTube tab to read `visitor_data` (cached in session storage), reads the captured
|
| 45 |
+
`pot`, and formats the PO token as `web.gvs+<token>`.
|
| 46 |
+
- **Auto-fill** β "Fill the Space tab" injects `fillSpaceFields` into the Space tab (all
|
| 47 |
+
frames, so it works both at `*.hf.space` and when embedded in `huggingface.co/spaces`).
|
| 48 |
+
It sets `#tm_pot_token` / `#tm_visitor_data` using the native value setter plus `input`
|
| 49 |
+
events so Gradio's Svelte bindings register the change. Requires the `*://*.hf.space/*`
|
| 50 |
+
host permission.
|
| 51 |
|
| 52 |
## Caveats
|
| 53 |
|
extension/manifest.json
CHANGED
|
@@ -4,7 +4,11 @@
|
|
| 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": [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"background": { "service_worker": "background.js" },
|
| 9 |
"action": {
|
| 10 |
"default_popup": "popup.html",
|
|
|
|
| 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": [
|
| 8 |
+
"*://*.youtube.com/*",
|
| 9 |
+
"*://*.googlevideo.com/*",
|
| 10 |
+
"*://*.hf.space/*"
|
| 11 |
+
],
|
| 12 |
"background": { "service_worker": "background.js" },
|
| 13 |
"action": {
|
| 14 |
"default_popup": "popup.html",
|
extension/popup.html
CHANGED
|
@@ -26,10 +26,11 @@
|
|
| 26 |
<textarea id="visField" rows="2" readonly placeholder="Cgtβ¦%3D%3D"></textarea>
|
| 27 |
|
| 28 |
<div class="row">
|
| 29 |
-
<button id="
|
| 30 |
-
<button id="copyVis">Copy visitor data</button>
|
| 31 |
</div>
|
| 32 |
<div class="row">
|
|
|
|
|
|
|
| 33 |
<button id="refresh">Refresh</button>
|
| 34 |
</div>
|
| 35 |
|
|
|
|
| 26 |
<textarea id="visField" rows="2" readonly placeholder="Cgtβ¦%3D%3D"></textarea>
|
| 27 |
|
| 28 |
<div class="row">
|
| 29 |
+
<button id="fillSpace"><b>‡ Fill the Space tab</b></button>
|
|
|
|
| 30 |
</div>
|
| 31 |
<div class="row">
|
| 32 |
+
<button id="copyPot">Copy PO token</button>
|
| 33 |
+
<button id="copyVis">Copy visitor data</button>
|
| 34 |
<button id="refresh">Refresh</button>
|
| 35 |
</div>
|
| 36 |
|
extension/popup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
// Popup: read visitor_data from the active YouTube tab (main world)
|
| 2 |
-
//
|
| 3 |
|
| 4 |
// Runs in the page's MAIN world via chrome.scripting; must be self-contained.
|
| 5 |
function readVisitorData() {
|
|
@@ -20,6 +20,33 @@ function readVisitorData() {
|
|
| 20 |
return null;
|
| 21 |
}
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
function setStatus(msg, kind) {
|
| 24 |
const el = document.getElementById("status");
|
| 25 |
el.textContent = msg;
|
|
@@ -40,7 +67,10 @@ async function load() {
|
|
| 40 |
const visEl = document.getElementById("visField");
|
| 41 |
|
| 42 |
const tab = await getActiveTab();
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
| 44 |
if (tab && isYouTube(tab.url)) {
|
| 45 |
try {
|
| 46 |
const res = await chrome.scripting.executeScript({
|
|
@@ -48,34 +78,43 @@ async function load() {
|
|
| 48 |
world: "MAIN",
|
| 49 |
func: readVisitorData,
|
| 50 |
});
|
| 51 |
-
|
| 52 |
} catch (e) {}
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
-
const
|
| 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
|
| 67 |
} else {
|
| 68 |
-
if (!visitor) notes.push("
|
| 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 β
|
| 73 |
}
|
| 74 |
-
if (
|
| 75 |
-
|
|
|
|
| 76 |
notes.push("Token captured ~" + mins + " min ago (they expire within hours).");
|
| 77 |
}
|
| 78 |
-
setStatus(notes.join(" ") || "Ready
|
| 79 |
}
|
| 80 |
|
| 81 |
async function copy(id, label) {
|
|
@@ -92,9 +131,41 @@ async function copy(id, label) {
|
|
| 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 |
});
|
|
|
|
| 1 |
+
// Popup: read visitor_data from the active YouTube tab (main world) + the captured gvs PO
|
| 2 |
+
// token from session storage, then either copy the values or auto-fill the Space's fields.
|
| 3 |
|
| 4 |
// Runs in the page's MAIN world via chrome.scripting; must be self-contained.
|
| 5 |
function readVisitorData() {
|
|
|
|
| 20 |
return null;
|
| 21 |
}
|
| 22 |
|
| 23 |
+
// Injected into the Space tab (default/isolated world) to fill the Gradio inputs by their
|
| 24 |
+
// elem_id. Uses the native value setter so Gradio's Svelte bindings register the change.
|
| 25 |
+
function fillSpaceFields(poToken, visitorData) {
|
| 26 |
+
function setVal(id, val) {
|
| 27 |
+
if (!val) return false;
|
| 28 |
+
const root = document.getElementById(id);
|
| 29 |
+
if (!root) return false;
|
| 30 |
+
const el = root.querySelector("textarea, input");
|
| 31 |
+
if (!el) return false;
|
| 32 |
+
const proto =
|
| 33 |
+
el.tagName === "TEXTAREA"
|
| 34 |
+
? window.HTMLTextAreaElement.prototype
|
| 35 |
+
: window.HTMLInputElement.prototype;
|
| 36 |
+
const setter = Object.getOwnPropertyDescriptor(proto, "value").set;
|
| 37 |
+
setter.call(el, val);
|
| 38 |
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
| 39 |
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
| 40 |
+
return true;
|
| 41 |
+
}
|
| 42 |
+
let n = 0;
|
| 43 |
+
if (setVal("tm_pot_token", poToken)) n++;
|
| 44 |
+
if (setVal("tm_visitor_data", visitorData)) n++;
|
| 45 |
+
return n;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const SPACE_RE = /(\.hf\.space)|(huggingface\.co\/spaces\/)/i;
|
| 49 |
+
|
| 50 |
function setStatus(msg, kind) {
|
| 51 |
const el = document.getElementById("status");
|
| 52 |
el.textContent = msg;
|
|
|
|
| 67 |
const visEl = document.getElementById("visField");
|
| 68 |
|
| 69 |
const tab = await getActiveTab();
|
| 70 |
+
|
| 71 |
+
// Read visitor_data live if we're on YouTube, and remember it for later (e.g. when the
|
| 72 |
+
// Space tab is the one in focus during fill).
|
| 73 |
+
let liveVisitor = null;
|
| 74 |
if (tab && isYouTube(tab.url)) {
|
| 75 |
try {
|
| 76 |
const res = await chrome.scripting.executeScript({
|
|
|
|
| 78 |
world: "MAIN",
|
| 79 |
func: readVisitorData,
|
| 80 |
});
|
| 81 |
+
liveVisitor = res && res[0] ? res[0].result : null;
|
| 82 |
} catch (e) {}
|
| 83 |
+
if (liveVisitor) await chrome.storage.session.set({ visitorData: liveVisitor });
|
| 84 |
}
|
| 85 |
|
| 86 |
+
const store = await chrome.storage.session.get([
|
| 87 |
"gvsPot",
|
| 88 |
"gvsClient",
|
| 89 |
"potTime",
|
| 90 |
+
"visitorData",
|
| 91 |
]);
|
| 92 |
+
const visitor = liveVisitor || store.visitorData || "";
|
| 93 |
|
| 94 |
+
visEl.value = visitor;
|
| 95 |
+
potEl.value = store.gvsPot ? "web.gvs+" + store.gvsPot : "";
|
| 96 |
+
|
| 97 |
+
// Is the Space open in some tab? Enable the fill button if so.
|
| 98 |
+
const all = await chrome.tabs.query({});
|
| 99 |
+
const spaceTabs = all.filter((t) => SPACE_RE.test(t.url || ""));
|
| 100 |
+
document.getElementById("fillSpace").disabled = spaceTabs.length === 0;
|
| 101 |
|
| 102 |
const notes = [];
|
| 103 |
+
if (!isYouTube(tab && tab.url) && !store.gvsPot) {
|
| 104 |
+
notes.push("Open a YouTube video, press play, then reopen this popup.");
|
| 105 |
} else {
|
| 106 |
+
if (!visitor) notes.push("No visitor data yet β open the popup once on the YouTube tab.");
|
| 107 |
+
if (!store.gvsPot) notes.push("No PO token yet β press play on a video, then reopen.");
|
| 108 |
}
|
| 109 |
+
if (store.gvsPot && store.gvsClient && store.gvsClient !== "WEB") {
|
| 110 |
+
notes.push("Captured client is '" + store.gvsClient + "', not WEB β use the normal web player.");
|
| 111 |
}
|
| 112 |
+
if (spaceTabs.length === 0) notes.push("Open the TutorialMaker Space in a tab to enable auto-fill.");
|
| 113 |
+
if (store.potTime) {
|
| 114 |
+
const mins = Math.round((Date.now() - store.potTime) / 60000);
|
| 115 |
notes.push("Token captured ~" + mins + " min ago (they expire within hours).");
|
| 116 |
}
|
| 117 |
+
setStatus(notes.join(" ") || "Ready.", notes.length ? "warn" : "ok");
|
| 118 |
}
|
| 119 |
|
| 120 |
async function copy(id, label) {
|
|
|
|
| 131 |
}
|
| 132 |
}
|
| 133 |
|
| 134 |
+
async function fillSpace() {
|
| 135 |
+
const poToken = document.getElementById("potField").value;
|
| 136 |
+
const visitorData = document.getElementById("visField").value;
|
| 137 |
+
if (!poToken && !visitorData) {
|
| 138 |
+
setStatus("Nothing to fill yet β capture the values first.", "warn");
|
| 139 |
+
return;
|
| 140 |
+
}
|
| 141 |
+
const all = await chrome.tabs.query({});
|
| 142 |
+
const spaceTabs = all.filter((t) => SPACE_RE.test(t.url || ""));
|
| 143 |
+
if (spaceTabs.length === 0) {
|
| 144 |
+
setStatus("No TutorialMaker Space tab found β open it first.", "warn");
|
| 145 |
+
return;
|
| 146 |
+
}
|
| 147 |
+
let filled = 0;
|
| 148 |
+
for (const t of spaceTabs) {
|
| 149 |
+
try {
|
| 150 |
+
const results = await chrome.scripting.executeScript({
|
| 151 |
+
target: { tabId: t.id, allFrames: true },
|
| 152 |
+
func: fillSpaceFields,
|
| 153 |
+
args: [poToken, visitorData],
|
| 154 |
+
});
|
| 155 |
+
for (const r of results) filled += r.result || 0;
|
| 156 |
+
} catch (e) {}
|
| 157 |
+
}
|
| 158 |
+
if (filled > 0) {
|
| 159 |
+
setStatus("Filled " + filled + " field(s) in the Space β (expand the panel to see).", "ok");
|
| 160 |
+
} else {
|
| 161 |
+
setStatus("Couldn't find the fields β make sure the Space finished loading.", "warn");
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
document.addEventListener("DOMContentLoaded", () => {
|
| 166 |
load();
|
| 167 |
document.getElementById("copyPot").addEventListener("click", () => copy("potField", "PO token"));
|
| 168 |
document.getElementById("copyVis").addEventListener("click", () => copy("visField", "visitor data"));
|
| 169 |
+
document.getElementById("fillSpace").addEventListener("click", fillSpace);
|
| 170 |
document.getElementById("refresh").addEventListener("click", load);
|
| 171 |
});
|